mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 00:53:21 +00:00 
			
		
		
		
	GUAC-442: Restrict access to WebSocket tunnel using filter (rather than RestrictedHttpServlet like the rest of guac).
This commit is contained in:
		| @@ -0,0 +1,93 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 Glyptodon LLC | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
|  * THE SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| package org.glyptodon.guacamole.net.basic; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import javax.servlet.Filter; | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.FilterConfig; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.ServletRequest; | ||||
| import javax.servlet.ServletResponse; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| import javax.servlet.http.HttpSession; | ||||
| import org.glyptodon.guacamole.net.auth.UserContext; | ||||
| import org.glyptodon.guacamole.protocol.GuacamoleStatus; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| /** | ||||
|  * Filter which enforces authentication. If no user context is associated with | ||||
|  * the current HTTP session, or no HTTP session exists, the request is denied. | ||||
|  * | ||||
|  * @author Michael Jumper | ||||
|  */ | ||||
| public class RestrictedFilter implements Filter { | ||||
|  | ||||
|     /** | ||||
|      * Logger for this class. | ||||
|      */ | ||||
|     private final Logger logger = LoggerFactory.getLogger(RestrictedFilter.class); | ||||
|  | ||||
|     @Override | ||||
|     public void init(FilterConfig config) throws ServletException { | ||||
|         // No configuration | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) | ||||
|     throws IOException, ServletException { | ||||
|        | ||||
|         HttpServletRequest request = (HttpServletRequest) req; | ||||
|         HttpServletResponse response = (HttpServletResponse) resp; | ||||
|  | ||||
|         // Pull user context from session | ||||
|         UserContext context = null; | ||||
|         HttpSession session = request.getSession(false); | ||||
|         if (session != null) | ||||
|             context = AuthenticatingFilter.getUserContext(session); | ||||
|  | ||||
|         // If authenticated, proceed with rest of chain | ||||
|         if (context != null) | ||||
|             chain.doFilter(req, resp); | ||||
|  | ||||
|         // Otherwise, deny entire request | ||||
|         else { | ||||
|             final GuacamoleStatus status = GuacamoleStatus.CLIENT_UNAUTHORIZED; | ||||
|             final String message = "Not authenticated"; | ||||
|  | ||||
|             logger.warn("Client request rejected: {}", message); | ||||
|             response.addHeader("Guacamole-Status-Code", Integer.toString(status.getGuacamoleStatusCode())); | ||||
|             response.addHeader("Guacamole-Error-Message", message); | ||||
|             response.sendError(status.getHttpStatusCode()); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void destroy() { | ||||
|         // No destruction needed | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,147 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (C) 2013 Glyptodon LLC | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
|  * THE SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| package org.glyptodon.guacamole.net.basic.websocket.jetty; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| import javax.servlet.http.HttpSession; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.GuacamoleServerException; | ||||
| import org.glyptodon.guacamole.net.auth.UserContext; | ||||
| import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; | ||||
| import org.eclipse.jetty.websocket.WebSocket; | ||||
| import org.eclipse.jetty.websocket.WebSocketServlet; | ||||
| import org.glyptodon.guacamole.net.basic.AuthenticatingFilter; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| /** | ||||
|  * A WebSocket servlet wrapped around an AuthenticatingHttpServlet. | ||||
|  * | ||||
|  * @author Michael Jumper | ||||
|  */ | ||||
| public abstract class AuthenticatingWebSocketServlet extends WebSocketServlet { | ||||
|  | ||||
|     /** | ||||
|      * Logger for this class. | ||||
|      */ | ||||
|     private final Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class); | ||||
|  | ||||
|     /** | ||||
|      * Wrapped authenticating servlet. | ||||
|      */ | ||||
|     private final RestrictedHttpServlet auth_servlet = new RestrictedHttpServlet() { | ||||
|  | ||||
|         @Override | ||||
|         protected void restrictedService(UserContext context, | ||||
|             HttpServletRequest request, HttpServletResponse response) | ||||
|             throws GuacamoleException { | ||||
|  | ||||
|             try { | ||||
|                 // If authenticated, service request | ||||
|                 service_websocket_request(request, response); | ||||
|             } | ||||
|             catch (IOException e) { | ||||
|                 throw new GuacamoleServerException( | ||||
|                         "Cannot service WebSocket request (I/O error).", e); | ||||
|             } | ||||
|             catch (ServletException e) { | ||||
|                 throw new GuacamoleServerException( | ||||
|                         "Cannot service WebSocket request (internal error).", e); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     @Override | ||||
|     public void init() throws ServletException { | ||||
|         super.init(); | ||||
|         auth_servlet.init(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void service(HttpServletRequest request, | ||||
|         HttpServletResponse response) | ||||
|         throws IOException, ServletException { | ||||
|  | ||||
|         // Authenticate all inbound requests | ||||
|         auth_servlet.service(request, response); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Actually services the given request, bypassing the service() override | ||||
|      * and the authentication scheme. | ||||
|      * | ||||
|      * @param request The HttpServletRequest to service. | ||||
|      * @param response The associated HttpServletResponse. | ||||
|      * @throws IOException If an I/O error occurs while handling the request. | ||||
|      * @throws ServletException If an internal error occurs while handling the | ||||
|      *                          request. | ||||
|      */ | ||||
|     private void service_websocket_request(HttpServletRequest request, | ||||
|         HttpServletResponse response) | ||||
|         throws IOException, ServletException { | ||||
|  | ||||
|         // Bypass override and service WebSocket request | ||||
|         super.service(request, response); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public WebSocket doWebSocketConnect(HttpServletRequest request, | ||||
|         String protocol) { | ||||
|  | ||||
|         // Get session and user context | ||||
|         HttpSession session = request.getSession(true); | ||||
|         UserContext context = AuthenticatingFilter.getUserContext(session); | ||||
|  | ||||
|         // Ensure user logged in | ||||
|         if (context == null) { | ||||
|             logger.warn("User no longer logged in upon WebSocket connect."); | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Connect WebSocket | ||||
|         return authenticatedConnect(context, request, protocol); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Function called after the credentials given in the request (if any) | ||||
|      * are authenticated. If the current session is not associated with | ||||
|      * valid credentials, this function will not be called. | ||||
|      * | ||||
|      * @param context The current UserContext. | ||||
|      * @param request The HttpServletRequest being serviced. | ||||
|      * @param protocol The protocol being used over the WebSocket connection. | ||||
|      * @return A connected WebSocket. | ||||
|      */ | ||||
|     protected abstract WebSocket authenticatedConnect( | ||||
|             UserContext context, | ||||
|             HttpServletRequest request, String protocol); | ||||
|  | ||||
| } | ||||
| @@ -25,36 +25,19 @@ package org.glyptodon.guacamole.net.basic.websocket.jetty; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.net.GuacamoleTunnel; | ||||
| import org.glyptodon.guacamole.net.auth.UserContext; | ||||
| import org.eclipse.jetty.websocket.WebSocket; | ||||
| import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; | ||||
|  | ||||
| /** | ||||
|  * Authenticating tunnel servlet implementation which uses WebSocket as a | ||||
|  * tunnel backend, rather than HTTP. | ||||
|  * Tunnel servlet implementation which uses WebSocket as a tunnel backend, | ||||
|  * rather than HTTP, properly parsing connection IDs included in the connection | ||||
|  * request. | ||||
|  */ | ||||
| public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet { | ||||
|  | ||||
|     /** | ||||
|      * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated | ||||
|      * requests. | ||||
|      */ | ||||
|     private GuacamoleWebSocketTunnelServlet tunnelServlet = | ||||
|             new GuacamoleWebSocketTunnelServlet() { | ||||
|  | ||||
|         @Override | ||||
|         protected GuacamoleTunnel doConnect(HttpServletRequest request) | ||||
|                 throws GuacamoleException { | ||||
|             return BasicTunnelRequestUtility.createTunnel(request); | ||||
|         } | ||||
|  | ||||
|     }; | ||||
| public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { | ||||
|  | ||||
|     @Override | ||||
|     protected WebSocket authenticatedConnect(UserContext context, | ||||
|         HttpServletRequest request, String protocol) { | ||||
|         return tunnelServlet.doWebSocketConnect(request, protocol); | ||||
|     protected GuacamoleTunnel doConnect(HttpServletRequest request) | ||||
|             throws GuacamoleException { | ||||
|         return BasicTunnelRequestUtility.createTunnel(request); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,161 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (C) 2013 Glyptodon LLC | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
|  * THE SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| package org.glyptodon.guacamole.net.basic.websocket.tomcat; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| import javax.servlet.http.HttpSession; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.GuacamoleServerException; | ||||
| import org.glyptodon.guacamole.net.auth.UserContext; | ||||
| import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; | ||||
| import org.apache.catalina.websocket.StreamInbound; | ||||
| import org.apache.catalina.websocket.WebSocketServlet; | ||||
| import org.glyptodon.guacamole.net.basic.AuthenticatingFilter; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| /** | ||||
|  * A WebSocket servlet wrapped around an AuthenticatingHttpServlet. | ||||
|  * | ||||
|  * @author Michael Jumper | ||||
|  */ | ||||
| public abstract class AuthenticatingWebSocketServlet extends WebSocketServlet { | ||||
|  | ||||
|     /** | ||||
|      * Logger for this class. | ||||
|      */ | ||||
|     private final Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class); | ||||
|  | ||||
|     /** | ||||
|      * Wrapped authenticating servlet. | ||||
|      */ | ||||
|     private final RestrictedHttpServlet auth_servlet = new RestrictedHttpServlet() { | ||||
|  | ||||
|         @Override | ||||
|         protected void restrictedService(UserContext context, | ||||
|             HttpServletRequest request, HttpServletResponse response) | ||||
|             throws GuacamoleException { | ||||
|  | ||||
|             try { | ||||
|                 // If authenticated, service request | ||||
|                 service_websocket_request(request, response); | ||||
|             } | ||||
|             catch (IOException e) { | ||||
|                 throw new GuacamoleServerException( | ||||
|                         "Cannot service WebSocket request (I/O error).", e); | ||||
|             } | ||||
|             catch (ServletException e) { | ||||
|                 throw new GuacamoleServerException( | ||||
|                         "Cannot service WebSocket request (internal error).", e); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     @Override | ||||
|     public void init() throws ServletException { | ||||
|         super.init(); | ||||
|         auth_servlet.init(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void service(HttpServletRequest request, | ||||
|         HttpServletResponse response) | ||||
|         throws IOException, ServletException { | ||||
|  | ||||
|         // Authenticate all inbound requests | ||||
|         auth_servlet.service(request, response); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Actually services the given request, bypassing the service() override | ||||
|      * and the authentication scheme. | ||||
|      * | ||||
|      * @param request The HttpServletRequest to service. | ||||
|      * @param response The associated HttpServletResponse. | ||||
|      * @throws IOException If an I/O error occurs while handling the request. | ||||
|      * @throws ServletException If an internal error occurs while handling the | ||||
|      *                          request. | ||||
|      */ | ||||
|     private void service_websocket_request(HttpServletRequest request, | ||||
|         HttpServletResponse response) | ||||
|         throws IOException, ServletException { | ||||
|  | ||||
|         // Bypass override and service WebSocket request | ||||
|         super.service(request, response); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String selectSubProtocol(List<String> subProtocols) { | ||||
|  | ||||
|         // Search for expected protocol | ||||
|         for (String protocol : subProtocols) | ||||
|             if ("guacamole".equals(protocol)) | ||||
|                 return "guacamole"; | ||||
|          | ||||
|         // Otherwise, fail | ||||
|         return null; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public StreamInbound createWebSocketInbound(String protocol, | ||||
|         HttpServletRequest request) { | ||||
|  | ||||
|         // Get session and user context | ||||
|         HttpSession session = request.getSession(true); | ||||
|         UserContext context = AuthenticatingFilter.getUserContext(session); | ||||
|  | ||||
|         // Ensure user logged in | ||||
|         if (context == null) { | ||||
|             logger.warn("User no longer logged in upon WebSocket connect."); | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Connect WebSocket | ||||
|         return authenticatedConnect(context, request, protocol); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Function called after the credentials given in the request (if any) | ||||
|      * are authenticated. If the current session is not associated with | ||||
|      * valid credentials, this function will not be called. | ||||
|      * | ||||
|      * @param context The current UserContext. | ||||
|      * @param request The HttpServletRequest being serviced. | ||||
|      * @param protocol The protocol being used over the WebSocket connection. | ||||
|      * @return A completed WebSocket connection. | ||||
|      */ | ||||
|     protected abstract StreamInbound authenticatedConnect( | ||||
|             UserContext context, | ||||
|             HttpServletRequest request, String protocol); | ||||
|  | ||||
| } | ||||
| @@ -25,36 +25,19 @@ package org.glyptodon.guacamole.net.basic.websocket.tomcat; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.net.GuacamoleTunnel; | ||||
| import org.glyptodon.guacamole.net.auth.UserContext; | ||||
| import org.apache.catalina.websocket.StreamInbound; | ||||
| import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; | ||||
|  | ||||
| /** | ||||
|  * Authenticating tunnel servlet implementation which uses WebSocket as a | ||||
|  * tunnel backend, rather than HTTP. | ||||
|  * Tunnel servlet implementation which uses WebSocket as a tunnel backend, | ||||
|  * rather than HTTP, properly parsing connection IDs included in the connection | ||||
|  * request. | ||||
|  */ | ||||
| public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet { | ||||
|  | ||||
|     /** | ||||
|      * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated | ||||
|      * requests. | ||||
|      */ | ||||
|     private GuacamoleWebSocketTunnelServlet tunnelServlet = | ||||
|             new GuacamoleWebSocketTunnelServlet() { | ||||
|  | ||||
|         @Override | ||||
|         protected GuacamoleTunnel doConnect(HttpServletRequest request) | ||||
|                 throws GuacamoleException { | ||||
|             return BasicTunnelRequestUtility.createTunnel(request); | ||||
|         } | ||||
|  | ||||
|     }; | ||||
| public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { | ||||
|  | ||||
|     @Override | ||||
|     protected StreamInbound authenticatedConnect(UserContext context, | ||||
|         HttpServletRequest request, String protocol) { | ||||
|         return tunnelServlet.createWebSocketInbound(protocol, request); | ||||
|     } | ||||
|     protected GuacamoleTunnel doConnect(HttpServletRequest request) | ||||
|             throws GuacamoleException { | ||||
|         return BasicTunnelRequestUtility.createTunnel(request); | ||||
|     }; | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import java.io.InputStream; | ||||
| import java.io.Reader; | ||||
| import java.nio.ByteBuffer; | ||||
| import java.nio.CharBuffer; | ||||
| import java.util.List; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import org.glyptodon.guacamole.GuacamoleException; | ||||
| import org.glyptodon.guacamole.io.GuacamoleReader; | ||||
| @@ -74,6 +75,19 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String selectSubProtocol(List<String> subProtocols) { | ||||
|  | ||||
|         // Search for expected protocol | ||||
|         for (String protocol : subProtocols) | ||||
|             if ("guacamole".equals(protocol)) | ||||
|                 return "guacamole"; | ||||
|          | ||||
|         // Otherwise, fail | ||||
|         return null; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public StreamInbound createWebSocketInbound(String protocol, HttpServletRequest request) { | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user