mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-10-18 14:36:11 +08:00
Add NetConf support, apply patch 10722
This commit is contained in:
@@ -112,6 +112,12 @@
|
||||
<Compile Include="..\Renci.SshNet\Common\Extensions.cs">
|
||||
<Link>Common\Extensions.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Renci.SshNet\Common\NetConfServerException.cs">
|
||||
<Link>Common\NetConfServerException.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Renci.SshNet\Common\NetConfServerException.NET40.cs">
|
||||
<Link>Common\NetConfServerException.NET40.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Renci.SshNet\Common\ObjectIdentifier.cs">
|
||||
<Link>Common\ObjectIdentifier.cs</Link>
|
||||
</Compile>
|
||||
@@ -403,6 +409,12 @@
|
||||
<Compile Include="..\Renci.SshNet\Messages\Transport\UnimplementedMessage.cs">
|
||||
<Link>Messages\Transport\UnimplementedMessage.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Renci.SshNet\NetConfClient.cs">
|
||||
<Link>NetConfClient.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Renci.SshNet\Netconf\NetConfSession.cs">
|
||||
<Link>Netconf\NetConfSession.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Renci.SshNet\NoneConnectionInfo.cs">
|
||||
<Link>NoneConnectionInfo.cs</Link>
|
||||
</Compile>
|
||||
@@ -697,7 +709,7 @@
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
|
||||
<UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Renci.SshNet.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when there is something wrong with the server capabilities.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public partial class NetConfServerException : SshException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SshAuthenticationException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
|
||||
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
|
||||
/// <exception cref="T:System.ArgumentNullException">The <paramref name="info"/> parameter is null. </exception>
|
||||
///
|
||||
/// <exception cref="T:System.Runtime.Serialization.SerializationException">The class name is null or <see cref="P:System.Exception.HResult"/> is zero (0). </exception>
|
||||
protected NetConfServerException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Renci.SshNet.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when there is something wrong with the server capabilities.
|
||||
/// </summary>
|
||||
public partial class NetConfServerException : SshException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NetConfServerException"/> class.
|
||||
/// </summary>
|
||||
public NetConfServerException()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NetConfServerException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
public NetConfServerException(string message)
|
||||
: base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NetConfServerException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
public NetConfServerException(string message, Exception innerException) :
|
||||
base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Renci.SshNet.Sftp;
|
||||
using System.Text;
|
||||
using Renci.SshNet.Common;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using Renci.SshNet.NetConf;
|
||||
using System.Xml;
|
||||
|
||||
namespace Renci.SshNet
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public partial class NetConfClient : BaseClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds SftpSession instance that used to communicate to the SFTP server
|
||||
/// </summary>
|
||||
private NetConfSession _netConfSession;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operation timeout.
|
||||
/// </summary>
|
||||
/// <value>The operation timeout.</value>
|
||||
public TimeSpan OperationTimeout { get; set; }
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SftpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connectionInfo">The connection info.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
|
||||
public NetConfClient(ConnectionInfo connectionInfo)
|
||||
: base(connectionInfo)
|
||||
{
|
||||
this.AutomaticMessageIdHandling = true;
|
||||
this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SftpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="host">Connection host.</param>
|
||||
/// <param name="port">Connection port.</param>
|
||||
/// <param name="username">Authentication username.</param>
|
||||
/// <param name="password">Authentication password.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="password"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is null or contains whitespace characters.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
|
||||
public NetConfClient(string host, int port, string username, string password)
|
||||
: this(new PasswordConnectionInfo(host, port, username, password))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SftpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="host">Connection host.</param>
|
||||
/// <param name="username">Authentication username.</param>
|
||||
/// <param name="password">Authentication password.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="password"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is null or contains whitespace characters.</exception>
|
||||
public NetConfClient(string host, string username, string password)
|
||||
: this(host, 22, username, password)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SftpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="host">Connection host.</param>
|
||||
/// <param name="port">Connection port.</param>
|
||||
/// <param name="username">Authentication username.</param>
|
||||
/// <param name="keyFiles">Authentication private key file(s) .</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is null or contains whitespace characters.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
|
||||
public NetConfClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
|
||||
: this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SftpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="host">Connection host.</param>
|
||||
/// <param name="username">Authentication username.</param>
|
||||
/// <param name="keyFiles">Authentication private key file(s) .</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is null or contains whitespace characters.</exception>
|
||||
public NetConfClient(string host, string username, params PrivateKeyFile[] keyFiles)
|
||||
: this(host, 22, username, keyFiles)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Gets NetConf server capabilities.
|
||||
/// </summary>
|
||||
public XmlDocument ServerCapabilities
|
||||
{
|
||||
get
|
||||
{
|
||||
this.EnsureConnection();
|
||||
return this._netConfSession.ServerCapabilities;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets NetConf client capabilities.
|
||||
/// </summary>
|
||||
public XmlDocument ClientCapabilities
|
||||
{
|
||||
get
|
||||
{
|
||||
this.EnsureConnection();
|
||||
return this._netConfSession.ClientCapabilities;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutomaticMessageIdHandling { get; set; }
|
||||
|
||||
public XmlDocument SendReceiveRpc(XmlDocument rpc)
|
||||
{
|
||||
this.EnsureConnection();
|
||||
return this._netConfSession.SendReceiveRpc(rpc, this.AutomaticMessageIdHandling);
|
||||
}
|
||||
|
||||
public XmlDocument SendReceiveRpc(string xml)
|
||||
{
|
||||
var rpc = new XmlDocument();
|
||||
rpc.LoadXml(xml);
|
||||
return SendReceiveRpc(rpc);
|
||||
}
|
||||
|
||||
public XmlDocument SendCloseRpc()
|
||||
{
|
||||
this.EnsureConnection();
|
||||
|
||||
XmlDocument rpc = new XmlDocument();
|
||||
|
||||
rpc.LoadXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rpc message-id=\"6666\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"><close-session/></rpc>");
|
||||
|
||||
return this._netConfSession.SendReceiveRpc(rpc, this.AutomaticMessageIdHandling);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when client is connected to the server.
|
||||
/// </summary>
|
||||
protected override void OnConnected()
|
||||
{
|
||||
base.OnConnected();
|
||||
|
||||
this._netConfSession = new NetConfSession(this.Session, this.OperationTimeout);
|
||||
|
||||
this._netConfSession.Connect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when client is disconnecting from the server.
|
||||
/// </summary>
|
||||
protected override void OnDisconnecting()
|
||||
{
|
||||
base.OnDisconnecting();
|
||||
|
||||
this._netConfSession.Disconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged ResourceMessages.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (this._netConfSession != null)
|
||||
{
|
||||
this._netConfSession.Dispose();
|
||||
this._netConfSession = null;
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Renci.SshNet.Channels;
|
||||
using Renci.SshNet.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Renci.SshNet.Sftp.Responses;
|
||||
using Renci.SshNet.Sftp.Requests;
|
||||
using Renci.SshNet.Sftp;
|
||||
using Renci.SshNet.Messages.Connection;
|
||||
using System.Xml;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Renci.SshNet.NetConf
|
||||
{
|
||||
internal class NetConfSession : SubsystemSession
|
||||
{
|
||||
private bool _usingFramingProtocol = false;
|
||||
|
||||
private const string _prompt = "]]>]]>";
|
||||
|
||||
private List<byte> _data = new List<byte>(32 * 1024);
|
||||
|
||||
private EventWaitHandle _serverCapabilitiesConfirmed = new AutoResetEvent(false);
|
||||
|
||||
private EventWaitHandle _rpcReplyReceived = new AutoResetEvent(false);
|
||||
|
||||
private StringBuilder _rpcReply = new StringBuilder();
|
||||
|
||||
private int _messageId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets NetConf server capabilities.
|
||||
/// </summary>
|
||||
public XmlDocument ServerCapabilities { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets NetConf client capabilities.
|
||||
/// </summary>
|
||||
public XmlDocument ClientCapabilities { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NetConfSession"/> class.
|
||||
/// </summary>
|
||||
/// <param name="session">The session.</param>
|
||||
/// <param name="operationTimeout">The operation timeout.</param>
|
||||
public NetConfSession(Session session, TimeSpan operationTimeout)
|
||||
: base(session, "netconf", operationTimeout)
|
||||
{
|
||||
ClientCapabilities = new XmlDocument();
|
||||
ClientCapabilities.LoadXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">" +
|
||||
"<capabilities>" +
|
||||
"<capability>" +
|
||||
"urn:ietf:params:netconf:base:1.0" +
|
||||
"</capability>" +
|
||||
"</capabilities>" +
|
||||
"</hello>");
|
||||
|
||||
}
|
||||
|
||||
public XmlDocument SendReceiveRpc(XmlDocument rpc, bool automaticMessageIdHandling)
|
||||
{
|
||||
XmlNamespaceManager ns = null;
|
||||
if (automaticMessageIdHandling)
|
||||
{
|
||||
_messageId++;
|
||||
ns = new XmlNamespaceManager(rpc.NameTable);
|
||||
ns.AddNamespace("nc", "urn:ietf:params:xml:ns:netconf:base:1.0");
|
||||
rpc.SelectSingleNode("/nc:rpc/@message-id", ns).Value = _messageId.ToString();
|
||||
}
|
||||
_rpcReply = new StringBuilder();
|
||||
_rpcReplyReceived.Reset();
|
||||
var reply = new XmlDocument();
|
||||
if (_usingFramingProtocol)
|
||||
{
|
||||
StringBuilder command = new StringBuilder(rpc.InnerXml.Length + 10);
|
||||
command.AppendFormat("\n#{0}\n", rpc.InnerXml.Length);
|
||||
command.Append(rpc.InnerXml);
|
||||
command.Append("\n##\n");
|
||||
SendData(new ChannelDataMessage(this.ChannelNumber, Encoding.UTF8.GetBytes(command.ToString())));
|
||||
this.WaitHandle(this._rpcReplyReceived, this._operationTimeout);
|
||||
reply.LoadXml(_rpcReply.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
SendData(new ChannelDataMessage(this.ChannelNumber, Encoding.UTF8.GetBytes(rpc.InnerXml + _prompt)));
|
||||
this.WaitHandle(this._rpcReplyReceived, this._operationTimeout);
|
||||
reply.LoadXml(_rpcReply.ToString());
|
||||
}
|
||||
if (automaticMessageIdHandling)
|
||||
{
|
||||
//string reply_id = rpc.SelectSingleNode("/nc:rpc-reply/@message-id", ns).Value;
|
||||
string reply_id = rpc.SelectSingleNode("/nc:rpc/@message-id", ns).Value;
|
||||
if (reply_id != _messageId.ToString())
|
||||
{
|
||||
throw new NetConfServerException("The rpc message id does not match the rpc-reply message id.");
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
protected override void OnChannelOpen()
|
||||
{
|
||||
string message = string.Format("{0}{1}", this.ClientCapabilities.InnerXml, _prompt);
|
||||
|
||||
this.SendData(new ChannelDataMessage(this.ChannelNumber, Encoding.UTF8.GetBytes(message)));
|
||||
|
||||
this.WaitHandle(this._serverCapabilitiesConfirmed, this._operationTimeout);
|
||||
}
|
||||
|
||||
protected override void OnDataReceived(uint dataTypeCode, byte[] data)
|
||||
{
|
||||
string chunk = Encoding.UTF8.GetString(data);
|
||||
|
||||
if (this.ServerCapabilities == null) // This must be server capabilities, old protocol
|
||||
{
|
||||
if (!chunk.EndsWith(_prompt))
|
||||
{
|
||||
throw new NetConfServerException("Server capabilities is not the first message received");
|
||||
}
|
||||
try
|
||||
{
|
||||
this.ServerCapabilities = new XmlDocument();
|
||||
this.ServerCapabilities.LoadXml(chunk.Replace(_prompt, ""));
|
||||
}
|
||||
catch (XmlException e)
|
||||
{
|
||||
throw new NetConfServerException("Server capabilities received are not well formed XML", e);
|
||||
}
|
||||
|
||||
XmlNamespaceManager ns = new XmlNamespaceManager(this.ServerCapabilities.NameTable);
|
||||
|
||||
ns.AddNamespace("nc", "urn:ietf:params:xml:ns:netconf:base:1.0");
|
||||
|
||||
this._usingFramingProtocol = (this.ServerCapabilities.SelectSingleNode("/nc:hello/nc:capabilities/nc:capability[text()='urn:ietf:params:netconf:base:1.1']", ns) != null);
|
||||
|
||||
this._serverCapabilitiesConfirmed.Set();
|
||||
}
|
||||
else if (this._usingFramingProtocol)
|
||||
{
|
||||
int position = 0;
|
||||
|
||||
for (; ; )
|
||||
{
|
||||
Match match = Regex.Match(chunk.Substring(position), @"\n#(?<length>\d+)\n");
|
||||
if (!match.Success)
|
||||
{
|
||||
break;
|
||||
}
|
||||
int fractionLength = Convert.ToInt32(match.Groups["length"].Value);
|
||||
this._rpcReply.Append(chunk, position + match.Index + match.Length, fractionLength);
|
||||
position += match.Index + match.Length + fractionLength;
|
||||
}
|
||||
if (Regex.IsMatch(chunk.Substring(position), @"\n##\n"))
|
||||
{
|
||||
this._rpcReplyReceived.Set();
|
||||
}
|
||||
}
|
||||
else // Old protocol
|
||||
{
|
||||
if (!chunk.EndsWith(_prompt))
|
||||
{
|
||||
throw new NetConfServerException("Server XML message does not end with the prompt " + _prompt);
|
||||
}
|
||||
this._rpcReply.Append(chunk.Replace(_prompt, ""));
|
||||
this._rpcReplyReceived.Set();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (this._serverCapabilitiesConfirmed != null)
|
||||
{
|
||||
this._serverCapabilitiesConfirmed.Dispose();
|
||||
this._serverCapabilitiesConfirmed = null;
|
||||
}
|
||||
|
||||
if (this._rpcReplyReceived != null)
|
||||
{
|
||||
this._rpcReplyReceived.Dispose();
|
||||
this._rpcReplyReceived = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@
|
||||
<Compile Include="Common\ChannelEventArgs.cs" />
|
||||
<Compile Include="Common\ChannelOpenFailedEventArgs.cs" />
|
||||
<Compile Include="Common\ChannelRequestEventArgs.cs" />
|
||||
<Compile Include="Common\NetConfServerException.NET40.cs" />
|
||||
<Compile Include="Common\DerData.cs" />
|
||||
<Compile Include="Common\ExceptionEventArgs.cs" />
|
||||
<Compile Include="Common\Extensions.cs">
|
||||
@@ -93,6 +94,7 @@
|
||||
<Compile Include="Common\Extensions.NET40.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Common\NetConfServerException.cs" />
|
||||
<Compile Include="Common\ObjectIdentifier.cs" />
|
||||
<Compile Include="Common\PipeStream.cs" />
|
||||
<Compile Include="Common\PortForwardEventArgs.cs">
|
||||
@@ -219,6 +221,8 @@
|
||||
<Compile Include="Messages\Transport\ServiceAcceptMessage.cs" />
|
||||
<Compile Include="Messages\Transport\ServiceRequestMessage.cs" />
|
||||
<Compile Include="Messages\Transport\UnimplementedMessage.cs" />
|
||||
<Compile Include="NetConfClient.cs" />
|
||||
<Compile Include="Netconf\NetConfSession.cs" />
|
||||
<Compile Include="NoneConnectionInfo.cs" />
|
||||
<Compile Include="PasswordConnectionInfo.cs" />
|
||||
<Compile Include="PasswordConnectionInfo.NET40.cs" />
|
||||
|
||||
Reference in New Issue
Block a user