001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.protocol.ws;
018
019import org.apache.wicket.Application;
020import org.apache.wicket.MetaDataKey;
021import org.apache.wicket.Page;
022import org.apache.wicket.protocol.ws.api.IWebSocketConnection;
023import org.apache.wicket.protocol.ws.api.IWebSocketConnectionFilter;
024import org.apache.wicket.protocol.ws.api.IWebSocketSession;
025import org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer;
026import org.apache.wicket.protocol.ws.api.ServletRequestCopy;
027import org.apache.wicket.protocol.ws.api.WebSocketConnectionFilterCollection;
028import org.apache.wicket.protocol.ws.api.WebSocketRequest;
029import org.apache.wicket.protocol.ws.api.WebSocketRequestHandler;
030import org.apache.wicket.protocol.ws.api.WebSocketResponse;
031import org.apache.wicket.protocol.ws.api.registry.IWebSocketConnectionRegistry;
032import org.apache.wicket.protocol.ws.api.registry.SimpleWebSocketConnectionRegistry;
033import org.apache.wicket.protocol.ws.concurrent.Executor;
034import org.apache.wicket.request.Url;
035import org.apache.wicket.request.cycle.RequestCycle;
036import org.apache.wicket.request.http.WebRequest;
037import org.apache.wicket.request.http.WebResponse;
038import org.apache.wicket.util.lang.Args;
039import org.apache.wicket.util.string.Strings;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043import jakarta.servlet.http.HttpServletRequest;
044import java.util.concurrent.SynchronousQueue;
045import java.util.concurrent.ThreadPoolExecutor;
046import java.util.concurrent.TimeUnit;
047import java.util.concurrent.atomic.AtomicInteger;
048import java.util.concurrent.atomic.AtomicReference;
049import java.util.function.Function;
050
051/**
052 * Web Socket related settings.
053 *
054 * More documentation is available about each setting in the setter method for the property.
055 */
056public class WebSocketSettings
057{
058        private static final Logger LOG = LoggerFactory.getLogger(WebSocketSettings.class);
059
060        private static final MetaDataKey<WebSocketSettings> KEY = new MetaDataKey<>()
061        {
062        };
063
064        /**
065         * A flag indicating whether JavaxWebSocketFilter is in use.
066         * When using JSR356 based implementations the ws:// url should not
067         * use the WicketFilter's filterPath because JSR356 Upgrade connections
068         * are never passed to the Servlet Filters.
069         */
070        private static boolean USING_JAVAX_WEB_SOCKET = false;
071
072        static
073        {
074                try
075                {
076                        Class.forName("org.apache.wicket.protocol.ws.javax.JavaxWebSocketFilter");
077                        USING_JAVAX_WEB_SOCKET = true;
078                        LOG.debug("Using JSR356 Native WebSocket implementation!");
079                } catch (ClassNotFoundException e)
080                {
081                        LOG.debug("Using non-JSR356 Native WebSocket implementation!");
082                }
083        }
084
085        private final AtomicReference<CharSequence> filterPrefix = new AtomicReference<>();
086        private final AtomicReference<CharSequence> contextPath = new AtomicReference<>();
087        private final AtomicReference<CharSequence> baseUrl = new AtomicReference<>();
088        private final AtomicInteger port = new AtomicInteger();
089        private final AtomicInteger securePort = new AtomicInteger();
090
091        /**
092         * Holds this WebSocketSettings in the Application's metadata.
093         * This way wicket-core module doesn't have reference to wicket-native-websocket.
094         */
095        public static final class Holder
096        {
097                public static WebSocketSettings get(Application application)
098                {
099                        WebSocketSettings settings = application.getMetaData(KEY);
100                        if (settings == null)
101                        {
102                                synchronized (application)
103                                {
104                                        settings = application.getMetaData(KEY);
105                                        if (settings == null)
106                                        {
107                                                settings = new WebSocketSettings();
108                                                set(application, settings);
109                                        }
110                                }
111                        }
112                        return settings;
113                }
114
115                public static void set(Application application, WebSocketSettings settings)
116                {
117                        application.setMetaData(KEY, settings);
118                }
119        }
120
121        /**
122         * The executor that handles the processing of Web Socket push message broadcasts.
123         */
124        private Executor webSocketPushMessageExecutor = new WebSocketPushMessageExecutor();
125
126        /**
127         * The executor that handles broadcast of the {@link org.apache.wicket.protocol.ws.api.event.WebSocketPayload}
128         * via Wicket's event bus.
129         */
130        private Executor sendPayloadExecutor = new SameThreadExecutor();
131
132        /**
133         * Tracks all currently connected WebSocket clients
134         */
135        private IWebSocketConnectionRegistry connectionRegistry = new SimpleWebSocketConnectionRegistry();
136
137        /**
138         * A filter that may reject an incoming connection
139         */
140        private IWebSocketConnectionFilter connectionFilter;
141
142        /**
143         * A {@link org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer} that allows to configure
144         * {@link org.apache.wicket.protocol.ws.api.IWebSocketSession}s.
145         */
146        private IWebSocketSessionConfigurer socketSessionConfigurer = new IWebSocketSessionConfigurer() {
147                @Override
148                public void configureSession(IWebSocketSession webSocketSession) {
149                        // does nothing by default
150                }
151        };
152
153        /**
154         * A function that decides whether to notify the page/resource on
155         * web socket connection closed event.
156         * The page notification leads to deserialization of the page instance from
157         * the page store and sometimes this is not wanted.
158         */
159        private Function<Integer, Boolean> notifyOnCloseEvent = (code) -> true;
160
161        /**
162         * Flag that allows to use asynchronous push. By default, it is set to <code>false</code>.
163         */
164        private boolean asynchronousPush = false;
165
166        /**
167         * The timeout to use for asynchronous push. By default, it is -1 which means use timeout configured by
168         * server implementation.
169         */
170        private long asynchronousPushTimeout = -1;
171
172        public boolean shouldNotifyOnCloseEvent(int closeCode)
173        {
174                return notifyOnCloseEvent == null || notifyOnCloseEvent.apply(closeCode);
175        }
176
177        public void setNotifyOnCloseEvent(Function<Integer, Boolean> notifyOnCloseEvent)
178        {
179                this.notifyOnCloseEvent = notifyOnCloseEvent;
180        }
181
182        /**
183         * A function that decides whether to notify the page/resource on
184         * web socket error event.
185         * The page notification leads to deserialization of the page instance from
186         * the page store and sometimes this is not wanted.
187         */
188        private Function<Throwable, Boolean> notifyOnErrorEvent = (throwable) -> true;
189
190        public boolean shouldNotifyOnErrorEvent(Throwable throwable)
191        {
192                return notifyOnErrorEvent == null || notifyOnErrorEvent.apply(throwable);
193        }
194
195        public void setNotifyOnErrorEvent(Function<Throwable, Boolean> notifyOnErrorEvent)
196        {
197                this.notifyOnErrorEvent = notifyOnErrorEvent;
198        }
199
200        /**
201         * Set the executor for processing websocket push messages broadcasted to all sessions.
202         * Default executor does all the processing in the caller thread. Using a proper thread pool is adviced
203         * for applications that send push events from ajax calls to avoid page level deadlocks.
204         *
205         * @param executor
206         *            The executor used for processing push messages.
207         */
208        public WebSocketSettings setWebSocketPushMessageExecutor(Executor executor)
209        {
210                Args.notNull(executor, "executor");
211                this.webSocketPushMessageExecutor = executor;
212                return this;
213        }
214
215        /**
216         * @return the executor for processing websocket push messages broadcasted to all sessions.
217         */
218        public Executor getWebSocketPushMessageExecutor()
219        {
220                return webSocketPushMessageExecutor;
221        }
222
223        /**
224         * @return The registry that tracks all currently connected WebSocket clients
225         */
226        public IWebSocketConnectionRegistry getConnectionRegistry()
227        {
228                return connectionRegistry;
229        }
230
231        /**
232         * Sets the connection registry
233         *
234         * @param connectionRegistry
235         *              The registry that tracks all currently connected WebSocket clients
236         * @return {@code this}, for method chaining
237         */
238        public WebSocketSettings setConnectionRegistry(IWebSocketConnectionRegistry connectionRegistry)
239        {
240                Args.notNull(connectionRegistry, "connectionRegistry");
241                this.connectionRegistry = connectionRegistry;
242                return this;
243        }
244
245        /**
246         * The executor that broadcasts the {@link org.apache.wicket.protocol.ws.api.event.WebSocketPayload}
247         * via Wicket's event bus.
248         * Default executor does all the processing in the caller thread.
249         *
250         * @param sendPayloadExecutor
251         *            The executor used for broadcasting the events with web socket payloads to
252         *            {@link org.apache.wicket.protocol.ws.api.WebSocketBehavior}s and
253         *            {@link org.apache.wicket.protocol.ws.api.WebSocketResource}s.
254         */
255        public WebSocketSettings setSendPayloadExecutor(Executor sendPayloadExecutor)
256        {
257                Args.notNull(sendPayloadExecutor, "sendPayloadExecutor");
258                this.sendPayloadExecutor = sendPayloadExecutor;
259                return this;
260        }
261
262        /**
263         * The executor that broadcasts the {@link org.apache.wicket.protocol.ws.api.event.WebSocketPayload}
264         * via Wicket's event bus.
265         *
266         * @return
267         *            The executor used for broadcasting the events with web socket payloads to
268         *            {@link org.apache.wicket.protocol.ws.api.WebSocketBehavior}s and
269         *            {@link org.apache.wicket.protocol.ws.api.WebSocketResource}s.
270         */
271        public Executor getSendPayloadExecutor()
272        {
273                return sendPayloadExecutor;
274        }
275
276        /**
277         * Sets the IWebSocketSessionConfigurer
278         * @param socketSessionConfigurer A non-null {@link org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer}
279         */
280        public void setSocketSessionConfigurer(IWebSocketSessionConfigurer socketSessionConfigurer) {
281                Args.notNull(socketSessionConfigurer, "socketSessionConfigurer");
282                this.socketSessionConfigurer = socketSessionConfigurer;
283        }
284
285        /**
286         * @return returns the {@link org.apache.wicket.protocol.ws.api.IWebSocketSessionConfigurer}
287         */
288        public IWebSocketSessionConfigurer getSocketSessionConfigurer() {
289                return socketSessionConfigurer;
290        }
291
292        /**
293         * Sets the filter for checking the incoming connections
294         *
295         * @param connectionFilter
296         *              the filter for checking the incoming connections
297         * @see WebSocketConnectionFilterCollection
298         */
299        public void setConnectionFilter(IWebSocketConnectionFilter connectionFilter)
300        {
301                this.connectionFilter = connectionFilter;
302        }
303
304        /**
305         * @return the filter for checking the incoming connections
306         * @see WebSocketConnectionFilterCollection
307         */
308        public IWebSocketConnectionFilter getConnectionFilter()
309        {
310                return this.connectionFilter;
311        }
312
313        /**
314         * A factory method for the {@link org.apache.wicket.request.http.WebResponse}
315         * that should be used to write the response back to the client/browser
316         *
317         * @param connection
318         *              The active web socket connection
319         * @return the response object that should be used to write the response back to the client
320         */
321        public WebResponse newWebSocketResponse(IWebSocketConnection connection)
322        {
323                return newWebSocketResponse(connection, isAsynchronousPush(), getAsynchronousPushTimeout());
324        }
325
326        /**
327         * A factory method for the {@link org.apache.wicket.request.http.WebResponse}
328         * that should be used to write the response back to the client/browser
329         *
330         * @param connection
331         *              The active web socket connection
332         * @param asynchronousPush
333         *              Whether asynchronous push is wanted or not.
334     * @param timeout
335     *              The timeout to be used for push operations
336         * @return the response object that should be used to write the response back to the client
337         */
338        public WebResponse newWebSocketResponse(IWebSocketConnection connection, boolean asynchronousPush, long timeout)
339        {
340                return new WebSocketResponse(connection, asynchronousPush, timeout);
341        }
342
343        /**
344         * A factory method for creating instances of {@link org.apache.wicket.protocol.ws.api.WebSocketRequestHandler}
345         * for processing a web socket request
346         *
347         * @param page
348         *          The page with the web socket client. A dummy page in case of usage of
349         *          {@link org.apache.wicket.protocol.ws.api.WebSocketResource}
350         * @param connection
351         *          The active web socket connection
352         * @return a new instance of WebSocketRequestHandler for processing a web socket request
353         */
354        public WebSocketRequestHandler newWebSocketRequestHandler(Page page, IWebSocketConnection connection)
355        {
356                return new WebSocketRequestHandler(page, connection);
357        }
358
359        /**
360         * A factory method for the {@link org.apache.wicket.request.http.WebRequest}
361         * that should be used in the WebSocket processing request cycle
362         *
363         * @param request
364         *              The upgraded http request
365         * @param filterPath
366         *              The configured filter path of WicketFilter in web.xml
367         * @return the request object that should be used in the WebSocket processing request cycle
368         */
369        public WebRequest newWebSocketRequest(HttpServletRequest request, String filterPath)
370        {
371                return new WebSocketRequest(new ServletRequestCopy(request), filterPath);
372        }
373
374        public void setFilterPrefix(final CharSequence filterPrefix)
375        {
376                this.filterPrefix.set(filterPrefix);
377        }
378
379        public CharSequence getFilterPrefix()
380        {
381                if (filterPrefix.get() == null)
382                {
383                        if (USING_JAVAX_WEB_SOCKET)
384                        {
385                                filterPrefix.compareAndSet(null, "");
386                        }
387                        else
388                        {
389                                filterPrefix.compareAndSet(null, RequestCycle.get().getRequest().getFilterPath());
390                        }
391                }
392                return filterPrefix.get();
393        }
394
395        public void setContextPath(final CharSequence contextPath)
396        {
397                this.contextPath.set(contextPath);
398        }
399
400        public CharSequence getContextPath()
401        {
402                contextPath.compareAndSet(null, RequestCycle.get().getRequest().getContextPath());
403                return contextPath.get();
404        }
405
406        public void setBaseUrl(final CharSequence baseUrl)
407        {
408                this.baseUrl.set(baseUrl);
409        }
410
411        public CharSequence getBaseUrl()
412        {
413                if (baseUrl.get() == null)
414                {
415                        Url _baseUrl = RequestCycle.get().getUrlRenderer().getBaseUrl();
416                        return Strings.escapeMarkup(_baseUrl.toString());
417                }
418                return baseUrl.get();
419        }
420
421        /**
422         * Sets the port that should be used for <code>ws:</code> connections.
423         * If unset then the current HTTP port will be used.
424         *
425         * @param wsPort The custom port for WS connections
426         */
427        public void setPort(int wsPort)
428        {
429                this.port.set(wsPort);
430        }
431
432        /**
433         * @return The custom port for WS connections
434         */
435        public Integer getPort()
436        {
437                return port.get();
438        }
439
440        /**
441         * Sets the port that should be used for <code>wss:</code> connections.
442         * If unset then the current HTTPS port will be used.
443         *
444         * @param wssPort The custom port for WSS connections
445         */
446        public void setSecurePort(int wssPort)
447        {
448                this.securePort.set(wssPort);
449        }
450
451        /**
452         * @return The custom port for WSS connections
453         */
454        public Integer getSecurePort()
455        {
456                return securePort.get();
457        }
458
459        /**
460         * Simple executor that runs the tasks in the caller thread.
461         */
462        public static class SameThreadExecutor implements Executor
463        {
464                @Override
465                public void run(Runnable command)
466                {
467                        command.run();
468                }
469        }
470
471        public static class WebSocketPushMessageExecutor implements Executor
472        {
473                /**
474                 * An executor that should be used when the WebSocket message is pushed
475                 * from non-http worker thread.
476                 */
477                private final java.util.concurrent.Executor nonHttpRequestExecutor;
478
479                /**
480                 * An executor that is used when the WebSocket push is initiated in
481                 * http worker thread. In this case the WebSocket processing should be
482                 * off-loaded to a different thread that should wait for the page instance
483                 * lock.
484                 */
485                private final java.util.concurrent.Executor httpRequestExecutor;
486
487                /**
488                 * For non-http worker threads pushes the WebSocket runnable in the same request.
489                 * For http worker threads uses an elastic thread pool of 1-8 threads.
490                 *
491                 * Use {@link WebSocketPushMessageExecutor#WebSocketPushMessageExecutor(java.util.concurrent.Executor, java.util.concurrent.Executor)}
492                 * for custom behavior and/or settings
493                 */
494                public WebSocketPushMessageExecutor()
495                {
496                        this(Runnable::run, new ThreadPoolExecutor(1, 8,
497                                                                   60L, TimeUnit.SECONDS,
498                                                                   new SynchronousQueue<>(),
499                                                                   new ThreadFactory()));
500                }
501
502                public WebSocketPushMessageExecutor(java.util.concurrent.Executor nonHttpRequestExecutor, java.util.concurrent.Executor httpRequestExecutor)
503                {
504                        this.nonHttpRequestExecutor = nonHttpRequestExecutor;
505                        this.httpRequestExecutor = httpRequestExecutor;
506                }
507
508                @Override
509                public void run(final Runnable command)
510                {
511                        if (RequestCycle.get() != null)
512                        {
513                                httpRequestExecutor.execute(command);
514                        }
515                        else
516                        {
517                                nonHttpRequestExecutor.execute(command);
518                        }
519                }
520        }
521
522        public static class ThreadFactory implements java.util.concurrent.ThreadFactory
523        {
524                private final AtomicInteger counter = new AtomicInteger();
525
526                @Override
527                public Thread newThread(final Runnable r)
528                {
529                        return new Thread(r, "Wicket-WebSocket-HttpRequest-Thread-" + counter.getAndIncrement());
530                }
531        }
532
533        public void setAsynchronousPush(boolean asynchronousPush)
534        {
535                this.asynchronousPush = asynchronousPush;
536        }
537
538        public boolean isAsynchronousPush()
539        {
540                return asynchronousPush;
541        }
542
543        public void setAsynchronousPushTimeout(long asynchronousPushTimeout)
544        {
545                this.asynchronousPushTimeout = asynchronousPushTimeout;
546        }
547
548        public long getAsynchronousPushTimeout()
549        {
550                return asynchronousPushTimeout;
551        }
552}