2424import java .io .InputStream ;
2525import java .lang .management .ManagementFactory ;
2626import java .net .URL ;
27+ import java .nio .file .Files ;
28+ import java .nio .file .Path ;
29+ import java .nio .file .Paths ;
2730import java .util .Arrays ;
31+ import java .util .EnumSet ;
32+ import java .util .List ;
2833import java .util .Properties ;
2934
30- import com .cloud .api .ApiServer ;
35+ import javax .servlet .DispatcherType ;
36+
3137import org .apache .commons .daemon .Daemon ;
3238import org .apache .commons .daemon .DaemonContext ;
3339import org .apache .commons .lang3 .StringUtils ;
40+ import org .apache .logging .log4j .LogManager ;
41+ import org .apache .logging .log4j .Logger ;
3442import org .eclipse .jetty .jmx .MBeanContainer ;
3543import org .eclipse .jetty .server .ForwardedRequestCustomizer ;
44+ import org .eclipse .jetty .server .Handler ;
3645import org .eclipse .jetty .server .HttpConfiguration ;
3746import org .eclipse .jetty .server .HttpConnectionFactory ;
3847import org .eclipse .jetty .server .RequestLog ;
4655import org .eclipse .jetty .server .handler .RequestLogHandler ;
4756import org .eclipse .jetty .server .handler .gzip .GzipHandler ;
4857import org .eclipse .jetty .server .session .SessionHandler ;
58+ import org .eclipse .jetty .servlet .DefaultServlet ;
59+ import org .eclipse .jetty .servlet .FilterHolder ;
60+ import org .eclipse .jetty .servlet .ServletContextHandler ;
61+ import org .eclipse .jetty .servlet .ServletHolder ;
62+ import org .eclipse .jetty .servlets .GzipFilter ;
63+ import org .eclipse .jetty .util .resource .Resource ;
4964import org .eclipse .jetty .util .ssl .KeyStoreScanner ;
5065import org .eclipse .jetty .util .ssl .SslContextFactory ;
5166import org .eclipse .jetty .util .thread .QueuedThreadPool ;
5267import org .eclipse .jetty .util .thread .ScheduledExecutorScheduler ;
5368import org .eclipse .jetty .webapp .WebAppContext ;
54- import org .apache .logging .log4j .Logger ;
55- import org .apache .logging .log4j .LogManager ;
5669
70+ import com .cloud .api .ApiServer ;
5771import com .cloud .utils .Pair ;
5872import com .cloud .utils .PropertiesUtil ;
5973import com .cloud .utils .server .ServerProperties ;
@@ -89,6 +103,12 @@ public class ServerDaemon implements Daemon {
89103 private static final String THREADS_MIN = "threads.min" ;
90104 private static final String THREADS_MAX = "threads.max" ;
91105
106+ public static final String SHARE_ENABLED = "share.enabled" ;
107+ public static final String SHARE_BASE_DIR = "share.base.dir" ;
108+ public static final String SHARE_CACHE_CONTROL = "share.cache.control" ;
109+ public static final String SHARE_DIR_ALLOWED = "share.dir.allowed" ;
110+ public static final String SHARE_SECRET = "share.secret" ;
111+
92112 ////////////////////////////////////////////////////////
93113 /////////////// Server Configuration ///////////////////
94114 ////////////////////////////////////////////////////////
@@ -111,6 +131,12 @@ public class ServerDaemon implements Daemon {
111131 private int minThreads ;
112132 private int maxThreads ;
113133
134+ private boolean shareEnabled = false ;
135+ private String shareBaseDir ;
136+ private String shareCacheCtl ;
137+ private boolean shareDirList = false ;
138+ private String shareSecret ;
139+
114140 //////////////////////////////////////////////////
115141 /////////////// Public methods ///////////////////
116142 //////////////////////////////////////////////////
@@ -121,6 +147,20 @@ public static void main(final String... anArgs) throws Exception {
121147 daemon .start ();
122148 }
123149
150+ protected void initShareConfigFromProperties (Properties properties ) {
151+ setShareEnabled (Boolean .parseBoolean (properties .getProperty (SHARE_ENABLED , "true" )));
152+ String defaultBaseDir = System .getProperty ("user.home" ) + File .separator + "share" ;
153+ boolean isMavenRun = ManagementFactory .getRuntimeMXBean ().getInputArguments ().toString ().contains ("org.codehaus.plexus.classworlds.launcher.Launcher" );
154+ if (isMavenRun ) {
155+ // when running from maven, use a share directory below the current working directory
156+ defaultBaseDir = System .getProperty ("user.dir" ) + File .separator + "share" ;
157+ }
158+ setShareBaseDir (properties .getProperty (SHARE_BASE_DIR , defaultBaseDir ));
159+ setShareCacheCtl (properties .getProperty (SHARE_CACHE_CONTROL , "public,max-age=86400,immutable" ));
160+ setShareDirList (Boolean .parseBoolean (properties .getProperty (SHARE_DIR_ALLOWED , "false" )));
161+ setShareSecret (properties .getProperty (SHARE_SECRET , "change-me" ));
162+ }
163+
124164 @ Override
125165 public void init (final DaemonContext context ) {
126166 final File confFile = PropertiesUtil .findConfigFile ("server.properties" );
@@ -153,6 +193,7 @@ public void init(final DaemonContext context) {
153193 setMaxFormKeys (Integer .valueOf (properties .getProperty (REQUEST_MAX_FORM_KEYS_KEY , String .valueOf (DEFAULT_REQUEST_MAX_FORM_KEYS ))));
154194 setMinThreads (Integer .valueOf (properties .getProperty (THREADS_MIN , "10" )));
155195 setMaxThreads (Integer .valueOf (properties .getProperty (THREADS_MAX , "500" )));
196+ initShareConfigFromProperties (properties );
156197 } catch (final IOException e ) {
157198 logger .warn ("Failed to read configuration from server.properties file" , e );
158199 } finally {
@@ -288,6 +329,57 @@ private void createHttpsConnector(final HttpConfiguration httpConfig) {
288329 }
289330 }
290331
332+ /**
333+ * Creates a Jetty context at /share to serve static files for modules (e.g. Extensions Framework).
334+ * Controlled via system properties (can be converted to ConfigKeys later):
335+ * -Dcs.share.enabled=true|false
336+ * -Dcs.share.baseDir=/var/lib/cloudstack/share
337+ * -Dcs.share.cacheControl=public,max-age=86400,immutable
338+ * -Dcs.share.dirAllowed=false
339+ * -Dcs.share.requireToken=false
340+ * -Dcs.share.secret=change-me
341+ *
342+ * @return a configured Handler or null if disabled.
343+ */
344+ private Handler createShareContextHandler () throws IOException {
345+ if (!shareEnabled ) {
346+ logger .info ("cs.share.enabled=false; /share context not mounted" );
347+ return null ;
348+ }
349+
350+ final Path base = Paths .get (shareBaseDir );
351+ Files .createDirectories (base );
352+
353+ final ServletContextHandler shareCtx = new ServletContextHandler (ServletContextHandler .NO_SESSIONS );
354+ shareCtx .setContextPath ("/share" );
355+ shareCtx .setBaseResource (Resource .newResource (base .toAbsolutePath ().toUri ()));
356+
357+ // Efficient static file serving
358+ ServletHolder def = shareCtx .addServlet (DefaultServlet .class , "/*" );
359+ def .setInitParameter ("dirAllowed" , Boolean .toString (shareDirList ));
360+ def .setInitParameter ("etags" , "true" );
361+ def .setInitParameter ("cacheControl" , shareCacheCtl );
362+ def .setInitParameter ("useFileMappedBuffer" , "true" );
363+ def .setInitParameter ("acceptRanges" , "true" );
364+
365+ // Gzip using modern Jetty handler
366+ org .eclipse .jetty .server .handler .gzip .GzipHandler gzipHandler =
367+ new org .eclipse .jetty .server .handler .gzip .GzipHandler ();
368+ gzipHandler .setMinGzipSize (1024 );
369+ gzipHandler .setIncludedMimeTypes (
370+ "text/html" , "text/plain" , "text/css" , "text/javascript" ,
371+ "application/javascript" , "application/json" , "application/xml" );
372+ gzipHandler .setHandler (shareCtx );
373+
374+ // Optional signed-URL guard (path + "|" + exp => HMAC-SHA256, base64url)
375+ if (StringUtils .isNotBlank (shareSecret )) {
376+ shareCtx .addFilter (new FilterHolder (new ShareSignedUrlFilter (true , shareSecret )), "/*" , EnumSet .of (DispatcherType .REQUEST ));
377+ }
378+
379+ logger .info ("Mounted /share static context at baseDir={}" , base );
380+ return shareCtx ;
381+ }
382+
291383 private Pair <SessionHandler ,HandlerCollection > createHandlers () {
292384 final WebAppContext webApp = new WebAppContext ();
293385 webApp .setContextPath (contextPath );
@@ -318,8 +410,23 @@ private Pair<SessionHandler,HandlerCollection> createHandlers() {
318410 rootRedirect .setNewContextURL (contextPath );
319411 rootRedirect .setPermanent (true );
320412
413+ // Optional /share handler (served by createShareContextHandler)
414+ Handler shareHandler = null ;
415+ try {
416+ shareHandler = createShareContextHandler ();
417+ } catch (IOException e ) {
418+ logger .error ("Failed to initialize /share context" , e );
419+ }
420+
421+ List <Handler > handlers = new java .util .ArrayList <>();
422+ handlers .add (log );
423+ handlers .add (gzipHandler );
424+ if (shareHandler != null ) {
425+ handlers .add (shareHandler );
426+ }
321427 // Put rootRedirect at the end!
322- return new Pair <>(webApp .getSessionHandler (), new HandlerCollection (log , gzipHandler , rootRedirect ));
428+ handlers .add (rootRedirect );
429+ return new Pair <>(webApp .getSessionHandler (), new HandlerCollection (handlers .toArray (new Handler [0 ])));
323430 }
324431
325432 private RequestLog createRequestLog () {
@@ -408,4 +515,24 @@ public void setMinThreads(int minThreads) {
408515 public void setMaxThreads (int maxThreads ) {
409516 this .maxThreads = maxThreads ;
410517 }
518+
519+ public void setShareEnabled (boolean shareEnabled ) {
520+ this .shareEnabled = shareEnabled ;
521+ }
522+
523+ public void setShareBaseDir (String shareBaseDir ) {
524+ this .shareBaseDir = shareBaseDir ;
525+ }
526+
527+ public void setShareCacheCtl (String shareCacheCtl ) {
528+ this .shareCacheCtl = shareCacheCtl ;
529+ }
530+
531+ public void setShareDirList (boolean shareDirList ) {
532+ this .shareDirList = shareDirList ;
533+ }
534+
535+ public void setShareSecret (String shareSecret ) {
536+ this .shareSecret = shareSecret ;
537+ }
411538}
0 commit comments