Skip to content

Commit 6276cd6

Browse files
committed
allow share context for lacal maven run, refactor
Signed-off-by: Abhishek Kumar <[email protected]>
1 parent 865c6fd commit 6276cd6

File tree

10 files changed

+262
-84
lines changed

10 files changed

+262
-84
lines changed

client/conf/server.properties.in

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ share.enabled=true
7373
# share.base.dir=
7474
# The cache control header value to be used for shared files. Default is public,max-age=86400,immutable
7575
# share.cache.control=public,max-age=86400,immutable
76-
# Allow or disallow directory listing when accessing a directory. Default is false
77-
# share.dir.allowed=false
7876
# Secret key for securing links using HMAC signature. If not set then links will not be signed. Default is change-me
7977
# It is recommended to change this value to a strong secret key in production
8078
share.secret=change-me

client/src/main/java/org/apache/cloudstack/ServerDaemon.java

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import javax.servlet.DispatcherType;
3636

37+
import org.apache.cloudstack.servlet.ShareSignedUrlFilter;
3738
import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
3839
import org.apache.commons.daemon.Daemon;
3940
import org.apache.commons.daemon.DaemonContext;
@@ -125,12 +126,6 @@ public class ServerDaemon implements Daemon {
125126
private int minThreads;
126127
private int maxThreads;
127128

128-
private boolean shareEnabled = false;
129-
private String shareBaseDir;
130-
private String shareCacheCtl;
131-
private boolean shareDirList = false;
132-
private String shareSecret;
133-
134129
//////////////////////////////////////////////////
135130
/////////////// Public methods ///////////////////
136131
//////////////////////////////////////////////////
@@ -141,22 +136,6 @@ public static void main(final String... anArgs) throws Exception {
141136
daemon.start();
142137
}
143138

144-
protected void initShareConfigFromProperties() {
145-
setShareEnabled(ServerPropertiesUtil.getShareEnabled());
146-
setShareBaseDir(ServerPropertiesUtil.getShareBaseDirectory());
147-
setShareCacheCtl(ServerPropertiesUtil.getShareCacheControl());
148-
setShareDirList(ServerPropertiesUtil.getShareDirAllowed());
149-
setShareSecret(ServerPropertiesUtil.getShareSecret());
150-
151-
logger.info(String.format("/%s static context enabled=%s, baseDir=%s, dirList=%s, cacheCtl=%s, secret=%s",
152-
ServerPropertiesUtil.SHARE_DIR,
153-
shareEnabled,
154-
shareBaseDir,
155-
shareDirList,
156-
shareCacheCtl,
157-
(StringUtils.isNotBlank(shareSecret) ? "configured" : "not configured")));
158-
}
159-
160139
@Override
161140
public void init(final DaemonContext context) {
162141
final File confFile = PropertiesUtil.findConfigFile("server.properties");
@@ -189,7 +168,6 @@ public void init(final DaemonContext context) {
189168
setMaxFormKeys(Integer.valueOf(properties.getProperty(REQUEST_MAX_FORM_KEYS_KEY, String.valueOf(DEFAULT_REQUEST_MAX_FORM_KEYS))));
190169
setMinThreads(Integer.valueOf(properties.getProperty(THREADS_MIN, "10")));
191170
setMaxThreads(Integer.valueOf(properties.getProperty(THREADS_MAX, "500")));
192-
initShareConfigFromProperties();
193171
} catch (final IOException e) {
194172
logger.warn("Failed to read configuration from server.properties file", e);
195173
} finally {
@@ -201,6 +179,13 @@ public void init(final DaemonContext context) {
201179
}
202180
logger.info(String.format("Initializing server daemon on %s, with http.enable=%s, http.port=%s, https.enable=%s, https.port=%s, context.path=%s",
203181
bindInterface, httpEnable, httpPort, httpsEnable, httpsPort, contextPath));
182+
183+
if (ServerPropertiesUtil.getShareEnabled()) {
184+
logger.info("/{} static context for file-sharing is enabled, baseDir={}, cacheCtl={}, secret={}",
185+
ServerPropertiesUtil.SHARE_DIR, ServerPropertiesUtil.getShareBaseDirectory(),
186+
ServerPropertiesUtil.getShareCacheControl(),
187+
(StringUtils.isNotBlank(ServerPropertiesUtil.getShareSecret()) ? "configured" : "not configured"));
188+
}
204189
}
205190

206191
@Override
@@ -332,12 +317,12 @@ private void createHttpsConnector(final HttpConfiguration httpConfig) {
332317
* @return a configured Handler or null if disabled.
333318
*/
334319
private Handler createShareContextHandler() throws IOException {
335-
if (!shareEnabled) {
320+
if (!ServerPropertiesUtil.getShareEnabled()) {
336321
logger.info("/{} context not mounted", ServerPropertiesUtil.SHARE_DIR);
337322
return null;
338323
}
339324

340-
final Path base = Paths.get(shareBaseDir);
325+
final Path base = Paths.get(ServerPropertiesUtil.getShareBaseDirectory());
341326
Files.createDirectories(base);
342327

343328
final ServletContextHandler shareCtx = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
@@ -346,9 +331,9 @@ private Handler createShareContextHandler() throws IOException {
346331

347332
// Efficient static file serving
348333
ServletHolder def = shareCtx.addServlet(DefaultServlet.class, "/*");
349-
def.setInitParameter("dirAllowed", Boolean.toString(shareDirList));
334+
def.setInitParameter("dirAllowed", "false");
350335
def.setInitParameter("etags", "true");
351-
def.setInitParameter("cacheControl", shareCacheCtl);
336+
def.setInitParameter("cacheControl", ServerPropertiesUtil.getShareCacheControl());
352337
def.setInitParameter("useFileMappedBuffer", "true");
353338
def.setInitParameter("acceptRanges", "true");
354339

@@ -360,12 +345,8 @@ private Handler createShareContextHandler() throws IOException {
360345
"text/html", "text/plain", "text/css", "text/javascript",
361346
"application/javascript", "application/json", "application/xml");
362347
gzipHandler.setHandler(shareCtx);
363-
364-
// Optional signed-URL guard (path + "|" + exp => HMAC-SHA256, base64url)
365-
if (StringUtils.isNotBlank(shareSecret)) {
366-
shareCtx.addFilter(new FilterHolder(new ShareSignedUrlFilter(shareSecret)),
367-
"/*", EnumSet.of(DispatcherType.REQUEST));
368-
}
348+
shareCtx.addFilter(new FilterHolder(new ShareSignedUrlFilter()), "/*",
349+
EnumSet.of(DispatcherType.REQUEST));
369350

370351
logger.info("Mounted /{} static context at baseDir={}", ServerPropertiesUtil.SHARE_DIR, base);
371352
return shareCtx;
@@ -506,24 +487,4 @@ public void setMinThreads(int minThreads) {
506487
public void setMaxThreads(int maxThreads) {
507488
this.maxThreads = maxThreads;
508489
}
509-
510-
public void setShareEnabled(boolean shareEnabled) {
511-
this.shareEnabled = shareEnabled;
512-
}
513-
514-
public void setShareBaseDir(String shareBaseDir) {
515-
this.shareBaseDir = shareBaseDir;
516-
}
517-
518-
public void setShareCacheCtl(String shareCacheCtl) {
519-
this.shareCacheCtl = shareCacheCtl;
520-
}
521-
522-
public void setShareDirList(boolean shareDirList) {
523-
this.shareDirList = shareDirList;
524-
}
525-
526-
public void setShareSecret(String shareSecret) {
527-
this.shareSecret = shareSecret;
528-
}
529490
}

client/src/main/webapp/WEB-INF/web.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@
5454
<load-on-startup>6</load-on-startup>
5555
</servlet>
5656

57+
<servlet>
58+
<servlet-name>shareServlet</servlet-name>
59+
<servlet-class>org.apache.cloudstack.servlet.ShareServlet</servlet-class>
60+
<load-on-startup>7</load-on-startup>
61+
</servlet>
62+
5763
<servlet-mapping>
5864
<servlet-name>apiServlet</servlet-name>
5965
<url-pattern>/api/*</url-pattern>
@@ -64,9 +70,24 @@
6470
<url-pattern>/console</url-pattern>
6571
</servlet-mapping>
6672

73+
<servlet-mapping>
74+
<servlet-name>shareServlet</servlet-name>
75+
<url-pattern>/share/*</url-pattern>
76+
</servlet-mapping>
77+
6778
<error-page>
6879
<exception-type>java.lang.Exception</exception-type>
6980
<location>/error.html</location>
7081
</error-page>
7182

83+
<filter>
84+
<filter-name>share-signed-url</filter-name>
85+
<filter-class>org.apache.cloudstack.servlet.ShareSignedUrlFilter</filter-class>
86+
</filter>
87+
<filter-mapping>
88+
<filter-name>share-signed-url</filter-name>
89+
<url-pattern>/share/*</url-pattern>
90+
<dispatcher>REQUEST</dispatcher>
91+
</filter-mapping>
92+
7293
</web-app>

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsFilesystemManagerImpl.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,12 @@ protected static String getFileExtension(File file) {
150150
return (lastDot == -1) ? "" : name.substring(lastDot + 1);
151151
}
152152

153-
protected Path getExtensionRootPath(String extensionName) {
153+
protected Path getExtensionRootPath(String extensionName, String extensionRelativePath) {
154+
if (StringUtils.isNotBlank(extensionRelativePath)) {
155+
Path extensionsPath = Paths.get(extensionsDirectory).toAbsolutePath().normalize();
156+
Path relativePath = Paths.get(extensionRelativePath);
157+
return extensionsPath.resolve(relativePath.getName(0)).normalize();
158+
}
154159
final String normalizedName = Extension.getDirectoryName(extensionName);
155160
final String extensionDir = extensionsDirectory + File.separator + normalizedName;
156161
return Path.of(extensionDir);
@@ -187,7 +192,7 @@ public String getExtensionsPath() {
187192

188193
@Override
189194
public Path getExtensionRootPath(Extension extension) {
190-
return getExtensionRootPath(extension.getName());
195+
return getExtensionRootPath(extension.getName(), extension.getRelativePath());
191196
}
192197

193198
@Override
@@ -226,7 +231,7 @@ public Map<String, String> getChecksumMapForExtension(String extensionName, Stri
226231
return null;
227232
}
228233
try {
229-
Path rootPath = getExtensionRootPath(extensionName);
234+
Path rootPath = getExtensionRootPath(extensionName, relativePath);
230235
Map<String, String> fileChecksums = new TreeMap<>();
231236
java.util.List<Path> files = new java.util.ArrayList<>();
232237
try (Stream<Path> stream = Files.walk(rootPath)) {

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsShareManagerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ protected String generateSignedArchiveUrl(ManagementServerHost managementServer,
287287
final long expiresAtEpochSec = System.currentTimeMillis() / 1000L + shareLinkValidityInterval;
288288
final String secretKey = ServerPropertiesUtil.getShareSecret();
289289
String archiveName = archivePath.getFileName().toString();
290-
String uriPath = String.format("/%s/%s/%s", ServerPropertiesUtil.SHARE_DIR, EXTENSIONS_SHARE_SUBDIR,
290+
String uriPath = String.format("%s/%s/%s", ServerPropertiesUtil.getShareUriPath(), EXTENSIONS_SHARE_SUBDIR,
291291
archiveName);
292292
String sig = "";
293293
if (StringUtils.isNotBlank(secretKey)) {

framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsFilesystemManagerImplTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ public void getChecksumMapForExtensionReturnsChecksumsForAllFiles() throws IOExc
302302
Path rootPath = tempDir.toPath();
303303
Path file1 = Files.createFile(rootPath.resolve("file1.txt"));
304304
Path file2 = Files.createFile(rootPath.resolve("file2.txt"));
305-
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName);
305+
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName, "");
306306
doReturn(rootPath.toString()).when(extensionsFilesystemManager).getExtensionCheckedPath(extensionName, "");
307307

308308
try (MockedStatic<DigestHelper> digestHelperMock = mockStatic(DigestHelper.class)) {
@@ -331,7 +331,7 @@ public void getChecksumMapForExtensionReturnsNullForBlankPath() {
331331
public void getChecksumMapForExtensionHandlesIOExceptionDuringFileWalk() {
332332
String extensionName = "test-extension";
333333
Path rootPath = tempDir.toPath();
334-
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName);
334+
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName, "");
335335
doReturn(rootPath.toString()).when(extensionsFilesystemManager).getExtensionCheckedPath(extensionName, "");
336336
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
337337
filesMock.when(() -> Files.walk(rootPath)).thenThrow(new IOException("File walk error"));
@@ -345,7 +345,7 @@ public void getChecksumMapForExtensionHandlesChecksumCalculationFailure() throws
345345
String extensionName = "test-extension";
346346
Path rootPath = tempDir.toPath();
347347
Path file1 = Files.createFile(rootPath.resolve("file1.txt"));
348-
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName);
348+
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName, "");
349349
doReturn(rootPath.toString()).when(extensionsFilesystemManager).getExtensionCheckedPath(extensionName, "");
350350
try (MockedStatic<DigestHelper> digestHelperMock = mockStatic(DigestHelper.class)) {
351351
digestHelperMock.when(() -> DigestHelper.calculateChecksum(file1.toFile())).thenThrow(new CloudRuntimeException("Checksum error"));
@@ -358,7 +358,7 @@ public void getChecksumMapForExtensionHandlesChecksumCalculationFailure() throws
358358
public void getChecksumMapForExtensionReturnsEmptyMapWhenNoFilesExist() {
359359
String extensionName = "test-extension";
360360
Path rootPath = tempDir.toPath();
361-
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName);
361+
doReturn(rootPath).when(extensionsFilesystemManager).getExtensionRootPath(extensionName, "");
362362
doReturn(rootPath.toString()).when(extensionsFilesystemManager).getExtensionCheckedPath(extensionName, "");
363363
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
364364
filesMock.when(() -> Files.walk(rootPath)).thenReturn(Stream.empty());
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.servlet;
19+
20+
import java.io.IOException;
21+
import java.io.OutputStream;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.nio.file.attribute.BasicFileAttributes;
26+
27+
import javax.servlet.ServletConfig;
28+
import javax.servlet.ServletException;
29+
import javax.servlet.http.HttpServlet;
30+
import javax.servlet.http.HttpServletRequest;
31+
import javax.servlet.http.HttpServletResponse;
32+
33+
import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
34+
import org.apache.commons.lang3.StringUtils;
35+
import org.apache.logging.log4j.LogManager;
36+
import org.apache.logging.log4j.Logger;
37+
import org.springframework.stereotype.Component;
38+
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
39+
40+
/**
41+
* A servlet to serve files from a configured share directory.
42+
* This is used only for local maven run. For production deployments, share context handling is in ServerDaemon.
43+
* Configuration properties read from server.properties:
44+
* <ul>
45+
* <li>share.enabled - Enable or disable the share servlet.</li>
46+
* <li>share.base.dir - The base directory from which files will be served.</li>
47+
* <li>share.cache.control - Cache-Control header value for served files.</li>
48+
* </ul>
49+
*/
50+
@Component("shareServlet")
51+
public class ShareServlet extends HttpServlet {
52+
private static final Logger LOG = LogManager.getLogger(ShareServlet.class);
53+
54+
private Path baseDir;
55+
private String cacheControl;
56+
57+
@Override
58+
public void init(final ServletConfig config) throws ServletException {
59+
super.init(config);
60+
SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, config.getServletContext());
61+
62+
if (!ServerPropertiesUtil.getShareEnabled()) {
63+
LOG.info("ShareServlet: share disabled, skipping initialization");
64+
return;
65+
}
66+
67+
try {
68+
baseDir = Paths.get(ServerPropertiesUtil.getShareBaseDirectory());
69+
Files.createDirectories(baseDir);
70+
cacheControl = ServerPropertiesUtil.getShareCacheControl();
71+
LOG.info("ShareServlet initialized at baseDir={}, cacheControl={}", baseDir, cacheControl);
72+
} catch (IOException e) {
73+
throw new ServletException("Failed to initialize ShareServlet", e);
74+
}
75+
}
76+
77+
@Override
78+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
79+
if (baseDir == null) {
80+
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Share feature disabled");
81+
return;
82+
}
83+
84+
// Resolve relative path safely
85+
final String relPath = StringUtils.removeStart(req.getPathInfo(), "/");
86+
if (relPath == null) {
87+
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
88+
return;
89+
}
90+
91+
final Path target = baseDir.resolve(relPath).normalize();
92+
if (!target.startsWith(baseDir)) {
93+
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
94+
return;
95+
}
96+
97+
if (!Files.exists(target) || !Files.isRegularFile(target)) {
98+
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
99+
return;
100+
}
101+
102+
// Add basic caching headers
103+
if (StringUtils.isNotBlank(cacheControl)) {
104+
resp.setHeader("Cache-Control", cacheControl);
105+
}
106+
resp.setHeader("Accept-Ranges", "bytes");
107+
108+
final BasicFileAttributes attrs = Files.readAttributes(target, BasicFileAttributes.class);
109+
resp.setDateHeader("Last-Modified", attrs.lastModifiedTime().toMillis());
110+
resp.setContentLengthLong(attrs.size());
111+
112+
final String mime = Files.probeContentType(target);
113+
if (mime != null) {
114+
resp.setContentType(mime);
115+
}
116+
117+
try (OutputStream out = resp.getOutputStream()) {
118+
Files.copy(target, out);
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)