mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2025-01-30 14:58:38 +00:00
Options unification: simple strings
Move all simple string options to the new scheme. Also regularise some names.
This commit is contained in:
parent
201c65960e
commit
18a6b66ba4
@ -27,7 +27,14 @@ class Options(optmanager.OptManager):
|
||||
"onboarding", True, bool,
|
||||
"Toggle the mitmproxy onboarding app."
|
||||
)
|
||||
self.add_option("onboarding_host", APP_HOST, str)
|
||||
self.add_option(
|
||||
"onboarding_host", APP_HOST, str,
|
||||
"""
|
||||
Domain to serve the onboarding app from. For transparent mode, use
|
||||
an IP when a DNS entry for the app domain is not present. Default:
|
||||
%s
|
||||
""" % APP_HOST
|
||||
)
|
||||
self.add_option(
|
||||
"onboarding_port", APP_PORT, int,
|
||||
help="Port to serve the onboarding app from."
|
||||
@ -64,7 +71,10 @@ class Options(optmanager.OptManager):
|
||||
self.add_option(
|
||||
"refresh_server_playback", True, bool,
|
||||
)
|
||||
self.add_option("rfile", None, Optional[str])
|
||||
self.add_option(
|
||||
"rfile", None, Optional[str],
|
||||
"Read flows from file."
|
||||
)
|
||||
self.add_option("scripts", [], Sequence[str])
|
||||
self.add_option(
|
||||
"showhost", False, bool,
|
||||
@ -75,8 +85,14 @@ class Options(optmanager.OptManager):
|
||||
self.add_option("server_replay_use_headers", [], Sequence[str])
|
||||
self.add_option("setheaders", [], Sequence[Union[Tuple[str, str, str], str]])
|
||||
self.add_option("server_replay", [], Sequence[str])
|
||||
self.add_option("stickycookie", None, Optional[str])
|
||||
self.add_option("stickyauth", None, Optional[str])
|
||||
self.add_option(
|
||||
"stickycookie", None, Optional[str],
|
||||
"Set sticky cookie filter. Matched against requests."
|
||||
)
|
||||
self.add_option(
|
||||
"stickyauth", None, Optional[str],
|
||||
"Set sticky auth filter. Matched against requests."
|
||||
)
|
||||
self.add_option(
|
||||
"stream_large_bodies", None, Optional[str],
|
||||
"""
|
||||
@ -109,8 +125,17 @@ class Options(optmanager.OptManager):
|
||||
"auth_nonanonymous", False, bool,
|
||||
"Allow access to any user long as a credentials are specified."
|
||||
)
|
||||
self.add_option("auth_singleuser", None, Optional[str])
|
||||
self.add_option("auth_htpasswd", None, Optional[str])
|
||||
self.add_option(
|
||||
"auth_singleuser", None, Optional[str],
|
||||
"""
|
||||
Allows access to a a single user, specified in the form
|
||||
username:password.
|
||||
"""
|
||||
)
|
||||
self.add_option(
|
||||
"auth_htpasswd", None, Optional[str],
|
||||
"Allow access to users specified in an Apache htpasswd file."
|
||||
)
|
||||
self.add_option(
|
||||
"add_upstream_certs_to_client_chain", False, bool,
|
||||
"Add all certificates of the upstream server to the certificate chain "
|
||||
@ -121,18 +146,36 @@ class Options(optmanager.OptManager):
|
||||
"Byte size limit of HTTP request and response bodies."
|
||||
" Understands k/m/g suffixes, i.e. 3m for 3 megabytes."
|
||||
)
|
||||
self.add_option("cadir", CA_DIR, str)
|
||||
self.add_option(
|
||||
"cadir", CA_DIR, str,
|
||||
"Location of the default mitmproxy CA files. (%s)" % CA_DIR
|
||||
)
|
||||
self.add_option("certs", [], Sequence[Tuple[str, str]])
|
||||
self.add_option("ciphers_client", DEFAULT_CLIENT_CIPHERS, str)
|
||||
self.add_option("ciphers_server", None, Optional[str])
|
||||
self.add_option("clientcerts", None, Optional[str])
|
||||
self.add_option(
|
||||
"ciphers_client", DEFAULT_CLIENT_CIPHERS, str,
|
||||
"Set supported ciphers for client connections. (OpenSSL Syntax)"
|
||||
)
|
||||
self.add_option(
|
||||
"ciphers_server", None, Optional[str],
|
||||
"Set supported ciphers for server connections. (OpenSSL Syntax)"
|
||||
)
|
||||
self.add_option(
|
||||
"client_certs", None, Optional[str],
|
||||
"Client certificate file or directory."
|
||||
)
|
||||
self.add_option("ignore_hosts", [], Sequence[str])
|
||||
self.add_option("listen_host", "", str)
|
||||
self.add_option(
|
||||
"listen_host", "", str,
|
||||
"Address to bind proxy to (defaults to all interfaces)"
|
||||
)
|
||||
self.add_option(
|
||||
"listen_port", LISTEN_PORT, int,
|
||||
"Proxy service port."
|
||||
)
|
||||
self.add_option("upstream_bind_address", "", str)
|
||||
self.add_option(
|
||||
"upstream_bind_address", "", str,
|
||||
"Address to bind upstream requests to (defaults to none)"
|
||||
)
|
||||
self.add_option("mode", "regular", str)
|
||||
self.add_option(
|
||||
"upstream_cert", True, bool,
|
||||
@ -172,23 +215,39 @@ class Options(optmanager.OptManager):
|
||||
"Combine with --upstream-bind-address to spoof a fixed source address."
|
||||
)
|
||||
self.add_option("upstream_server", None, Optional[str])
|
||||
self.add_option("upstream_auth", None, Optional[str])
|
||||
self.add_option(
|
||||
"upstream_auth", None, Optional[str],
|
||||
"""
|
||||
Add HTTP Basic authentcation to upstream proxy and reverse proxy
|
||||
requests. Format: username:password
|
||||
"""
|
||||
)
|
||||
self.add_option("ssl_version_client", "secure", str)
|
||||
self.add_option("ssl_version_server", "secure", str)
|
||||
self.add_option(
|
||||
"ssl_insecure", False, bool,
|
||||
"Do not verify upstream server SSL/TLS certificates."
|
||||
)
|
||||
self.add_option("ssl_verify_upstream_trusted_cadir", None, Optional[str])
|
||||
self.add_option("ssl_verify_upstream_trusted_ca", None, Optional[str])
|
||||
self.add_option(
|
||||
"ssl_verify_upstream_trusted_cadir", None, Optional[str],
|
||||
"Path to a directory of trusted CA certificates for upstream "
|
||||
"server verification prepared using the c_rehash tool."
|
||||
)
|
||||
self.add_option(
|
||||
"ssl_verify_upstream_trusted_ca", None, Optional[str],
|
||||
"Path to a PEM formatted trusted CA certificate."
|
||||
)
|
||||
self.add_option("tcp_hosts", [], Sequence[str])
|
||||
|
||||
self.add_option("intercept", None, Optional[str])
|
||||
self.add_option(
|
||||
"intercept", None, Optional[str],
|
||||
"Intercept filter expression."
|
||||
)
|
||||
|
||||
# Console options
|
||||
self.add_option(
|
||||
"console_eventlog", False, bool,
|
||||
help="Show event log."
|
||||
"Show event log."
|
||||
)
|
||||
self.add_option(
|
||||
"console_focus_follow", False, bool,
|
||||
@ -208,7 +267,10 @@ class Options(optmanager.OptManager):
|
||||
"console_order_reversed", False, bool,
|
||||
)
|
||||
|
||||
self.add_option("filter", None, Optional[str])
|
||||
self.add_option(
|
||||
"filter", None, Optional[str],
|
||||
"Filter view expression."
|
||||
)
|
||||
|
||||
# Web options
|
||||
self.add_option(
|
||||
@ -223,7 +285,10 @@ class Options(optmanager.OptManager):
|
||||
"web_port", 8081, int,
|
||||
"Mitmweb port."
|
||||
)
|
||||
self.add_option("web_iface", "127.0.0.1", str)
|
||||
self.add_option(
|
||||
"web_iface", "127.0.0.1", str,
|
||||
"Mitmweb interface."
|
||||
)
|
||||
|
||||
# Dump options
|
||||
self.add_option("filtstr", None, Optional[str])
|
||||
|
@ -59,7 +59,7 @@ class ProxyConfig:
|
||||
self.check_ignore = None
|
||||
self.check_tcp = None
|
||||
self.certstore = None
|
||||
self.clientcerts = None
|
||||
self.client_certs = None
|
||||
self.openssl_verification_mode_server = None
|
||||
self.configure(options, set(options.keys()))
|
||||
options.changed.connect(self.configure)
|
||||
@ -96,14 +96,14 @@ class ProxyConfig:
|
||||
CONF_BASENAME
|
||||
)
|
||||
|
||||
if options.clientcerts:
|
||||
clientcerts = os.path.expanduser(options.clientcerts)
|
||||
if not os.path.exists(clientcerts):
|
||||
if options.client_certs:
|
||||
client_certs = os.path.expanduser(options.client_certs)
|
||||
if not os.path.exists(client_certs):
|
||||
raise exceptions.OptionsError(
|
||||
"Client certificate path does not exist: %s" %
|
||||
options.clientcerts
|
||||
options.client_certs
|
||||
)
|
||||
self.clientcerts = clientcerts
|
||||
self.client_certs = client_certs
|
||||
|
||||
for spec, cert in options.certs:
|
||||
cert = os.path.expanduser(cert)
|
||||
|
@ -61,7 +61,7 @@ class RequestReplayThread(basethread.BaseThread):
|
||||
if resp.status_code != 200:
|
||||
raise exceptions.ReplayException("Upstream server refuses CONNECT request")
|
||||
server.establish_ssl(
|
||||
self.config.clientcerts,
|
||||
self.config.client_certs,
|
||||
sni=self.f.server_conn.sni
|
||||
)
|
||||
r.first_line_format = "relative"
|
||||
@ -76,7 +76,7 @@ class RequestReplayThread(basethread.BaseThread):
|
||||
server.connect()
|
||||
if r.scheme == "https":
|
||||
server.establish_ssl(
|
||||
self.config.clientcerts,
|
||||
self.config.client_certs,
|
||||
sni=self.f.server_conn.sni
|
||||
)
|
||||
r.first_line_format = "relative"
|
||||
|
@ -527,7 +527,7 @@ class TlsLayer(base.Layer):
|
||||
ciphers_server = ':'.join(ciphers_server)
|
||||
|
||||
self.server_conn.establish_ssl(
|
||||
self.config.clientcerts,
|
||||
self.config.client_certs,
|
||||
self.server_sni,
|
||||
method=self.config.openssl_method_server,
|
||||
options=self.config.openssl_options_server,
|
||||
|
@ -17,13 +17,6 @@ class ParseException(Exception):
|
||||
|
||||
|
||||
def get_common_options(args):
|
||||
stickycookie, stickyauth = None, None
|
||||
if args.stickycookie_filt:
|
||||
stickycookie = args.stickycookie_filt
|
||||
|
||||
if args.stickyauth_filt:
|
||||
stickyauth = args.stickyauth_filt
|
||||
|
||||
if args.streamfile and args.streamfile[0] == args.rfile:
|
||||
if args.streamfile[1] == "wb":
|
||||
raise exceptions.OptionsError(
|
||||
@ -101,8 +94,8 @@ def get_common_options(args):
|
||||
keep_host_header=args.keep_host_header,
|
||||
server_replay=args.server_replay,
|
||||
scripts=args.scripts,
|
||||
stickycookie=stickycookie,
|
||||
stickyauth=stickyauth,
|
||||
stickycookie=args.stickycookie,
|
||||
stickyauth=args.stickyauth,
|
||||
stream_large_bodies=args.stream_large_bodies,
|
||||
showhost=args.showhost,
|
||||
streamfile=args.streamfile[0] if args.streamfile else None,
|
||||
@ -123,9 +116,9 @@ def get_common_options(args):
|
||||
certs = certs,
|
||||
ciphers_client = args.ciphers_client,
|
||||
ciphers_server = args.ciphers_server,
|
||||
clientcerts = args.clientcerts,
|
||||
client_certs = args.client_certs,
|
||||
ignore_hosts = args.ignore_hosts,
|
||||
listen_host = args.listen_addr,
|
||||
listen_host = args.listen_host,
|
||||
listen_port = args.listen_port,
|
||||
upstream_bind_address = args.upstream_bind_address,
|
||||
mode = mode,
|
||||
@ -161,22 +154,14 @@ def basic_options(parser, opts):
|
||||
version=version.VERSION
|
||||
)
|
||||
opts.make_parser(parser, "anticache")
|
||||
parser.add_argument(
|
||||
"--cadir",
|
||||
action="store", type=str, dest="cadir",
|
||||
help="Location of the default mitmproxy CA files. (%s)" % options.CA_DIR
|
||||
)
|
||||
opts.make_parser(parser, "cadir")
|
||||
opts.make_parser(parser, "showhost")
|
||||
parser.add_argument(
|
||||
"-q", "--quiet",
|
||||
action="store_true", dest="quiet",
|
||||
help="Quiet."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--read-flows",
|
||||
action="store", dest="rfile",
|
||||
help="Read flows from file."
|
||||
)
|
||||
opts.make_parser(parser, "rfile")
|
||||
parser.add_argument(
|
||||
"-s", "--script",
|
||||
action="append", type=str, dest="scripts",
|
||||
@ -186,18 +171,8 @@ def basic_options(parser, opts):
|
||||
passed multiple times.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t", "--stickycookie",
|
||||
action="store",
|
||||
dest="stickycookie_filt",
|
||||
metavar="FILTER",
|
||||
help="Set sticky cookie filter. Matched against requests."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u", "--stickyauth",
|
||||
action="store", dest="stickyauth_filt", metavar="FILTER",
|
||||
help="Set sticky auth filter. Matched against requests."
|
||||
)
|
||||
opts.make_parser(parser, "stickycookie", metavar="FILTER")
|
||||
opts.make_parser(parser, "stickyauth", metavar="FILTER")
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_const", dest="verbose", const=3,
|
||||
@ -254,11 +229,7 @@ def proxy_modes(parser, opts):
|
||||
|
||||
def proxy_options(parser, opts):
|
||||
group = parser.add_argument_group("Proxy Options")
|
||||
group.add_argument(
|
||||
"-b", "--bind-address",
|
||||
action="store", type=str, dest="listen_addr",
|
||||
help="Address to bind proxy to (defaults to all interfaces)"
|
||||
)
|
||||
opts.make_parser(group, "listen_host")
|
||||
group.add_argument(
|
||||
"-I", "--ignore",
|
||||
action="append", type=str, dest="ignore_hosts",
|
||||
@ -292,24 +263,10 @@ def proxy_options(parser, opts):
|
||||
websocket = group.add_mutually_exclusive_group()
|
||||
opts.make_parser(websocket, "websocket")
|
||||
|
||||
parser.add_argument(
|
||||
"--upstream-auth",
|
||||
action="store", dest="upstream_auth",
|
||||
type=str,
|
||||
help="""
|
||||
Add HTTP Basic authentcation to upstream proxy and reverse proxy
|
||||
requests. Format: username:password
|
||||
"""
|
||||
)
|
||||
|
||||
opts.make_parser(group, "upstream_auth", metavar="USER:PASS")
|
||||
opts.make_parser(group, "rawtcp")
|
||||
|
||||
opts.make_parser(group, "spoof_source_address")
|
||||
group.add_argument(
|
||||
"--upstream-bind-address",
|
||||
action="store", type=str, dest="upstream_bind_address",
|
||||
help="Address to bind upstream requests to (defaults to none)"
|
||||
)
|
||||
opts.make_parser(group, "upstream_bind_address", metavar="ADDR")
|
||||
opts.make_parser(group, "keep_host_header")
|
||||
|
||||
|
||||
@ -328,35 +285,14 @@ def proxy_ssl_options(parser, opts):
|
||||
'in the PEM, it is used, else the default key in the conf dir is used. '
|
||||
'The PEM file should contain the full certificate chain, with the leaf certificate '
|
||||
'as the first entry. Can be passed multiple times.')
|
||||
group.add_argument(
|
||||
"--ciphers-client", action="store",
|
||||
type=str, dest="ciphers_client",
|
||||
help="Set supported ciphers for client connections. (OpenSSL Syntax)"
|
||||
)
|
||||
group.add_argument(
|
||||
"--ciphers-server", action="store",
|
||||
type=str, dest="ciphers_server",
|
||||
help="Set supported ciphers for server connections. (OpenSSL Syntax)"
|
||||
)
|
||||
group.add_argument(
|
||||
"--client-certs", action="store",
|
||||
type=str, dest="clientcerts",
|
||||
help="Client certificate file or directory."
|
||||
)
|
||||
opts.make_parser(group, "ciphers_server", metavar="CIPHERS")
|
||||
opts.make_parser(group, "ciphers_client", metavar="CIPHERS")
|
||||
opts.make_parser(group, "client_certs")
|
||||
opts.make_parser(group, "upstream_cert")
|
||||
opts.make_parser(group, "add_upstream_certs_to_client_chain")
|
||||
opts.make_parser(group, "ssl_insecure")
|
||||
group.add_argument(
|
||||
"--upstream-trusted-cadir", action="store",
|
||||
dest="ssl_verify_upstream_trusted_cadir",
|
||||
help="Path to a directory of trusted CA certificates for upstream "
|
||||
"server verification prepared using the c_rehash tool."
|
||||
)
|
||||
group.add_argument(
|
||||
"--upstream-trusted-ca", action="store",
|
||||
dest="ssl_verify_upstream_trusted_ca",
|
||||
help="Path to a PEM formatted trusted CA certificate."
|
||||
)
|
||||
opts.make_parser(group, "ssl_verify_upstream_trusted_cadir", metavar="PATH")
|
||||
opts.make_parser(group, "ssl_verify_upstream_trusted_ca", metavar="PATH")
|
||||
group.add_argument(
|
||||
"--ssl-version-client", dest="ssl_version_client",
|
||||
action="store",
|
||||
@ -375,16 +311,8 @@ def proxy_ssl_options(parser, opts):
|
||||
|
||||
def onboarding_app(parser, opts):
|
||||
group = parser.add_argument_group("Onboarding App")
|
||||
opts.make_parser(parser, "onboarding")
|
||||
group.add_argument(
|
||||
"--onboarding-host",
|
||||
action="store", dest="onboarding_host",
|
||||
help="""
|
||||
Domain to serve the onboarding app from. For transparent mode, use
|
||||
an IP when a DNS entry for the app domain is not present. Default:
|
||||
%s
|
||||
""" % options.APP_HOST
|
||||
)
|
||||
opts.make_parser(group, "onboarding")
|
||||
opts.make_parser(group, "onboarding_host", metavar="HOST")
|
||||
opts.make_parser(group, "onboarding_port", metavar="PORT")
|
||||
|
||||
|
||||
@ -488,21 +416,8 @@ def proxy_authentication(parser, opts):
|
||||
"""
|
||||
).add_mutually_exclusive_group()
|
||||
opts.make_parser(group, "auth_nonanonymous")
|
||||
group.add_argument(
|
||||
"--singleuser",
|
||||
action="store", dest="auth_singleuser", type=str,
|
||||
metavar="USER",
|
||||
help="""
|
||||
Allows access to a a single user, specified in the form
|
||||
username:password.
|
||||
"""
|
||||
)
|
||||
group.add_argument(
|
||||
"--htpasswd",
|
||||
action="store", dest="auth_htpasswd", type=str,
|
||||
metavar="PATH",
|
||||
help="Allow access to users specified in an Apache htpasswd file."
|
||||
)
|
||||
opts.make_parser(group, "auth_singleuser", metavar="USER:PASS")
|
||||
opts.make_parser(group, "auth_htpasswd", metavar="PATH")
|
||||
|
||||
|
||||
def common_options(parser, opts):
|
||||
@ -553,16 +468,8 @@ def mitmproxy(opts):
|
||||
"Filters",
|
||||
"See help in mitmproxy for filter expression syntax."
|
||||
)
|
||||
group.add_argument(
|
||||
"-i", "--intercept", action="store",
|
||||
type=str, dest="intercept",
|
||||
help="Intercept filter expression."
|
||||
)
|
||||
group.add_argument(
|
||||
"-f", "--filter", action="store",
|
||||
type=str, dest="filter",
|
||||
help="Filter view expression."
|
||||
)
|
||||
opts.make_parser(group, "intercept", metavar="FILTER")
|
||||
opts.make_parser(group, "filter", metavar="FILTER")
|
||||
return parser
|
||||
|
||||
|
||||
@ -588,13 +495,8 @@ def mitmweb(opts):
|
||||
|
||||
group = parser.add_argument_group("Mitmweb")
|
||||
opts.make_parser(group, "web_open_browser")
|
||||
opts.make_parser(parser, "web_port", metavar="PORT")
|
||||
group.add_argument(
|
||||
"--web-iface",
|
||||
action="store", dest="web_iface",
|
||||
metavar="IFACE",
|
||||
help="Mitmweb interface."
|
||||
)
|
||||
opts.make_parser(group, "web_port", metavar="PORT")
|
||||
opts.make_parser(group, "web_iface", metavar="INTERFACE")
|
||||
opts.make_parser(group, "web_debug")
|
||||
|
||||
common_options(parser, opts)
|
||||
@ -602,9 +504,5 @@ def mitmweb(opts):
|
||||
"Filters",
|
||||
"See help in mitmproxy for filter expression syntax."
|
||||
)
|
||||
group.add_argument(
|
||||
"-i", "--intercept", action="store",
|
||||
type=str, dest="intercept",
|
||||
help="Intercept filter expression."
|
||||
)
|
||||
opts.make_parser(group, "intercept", metavar="FILTER")
|
||||
return parser
|
||||
|
@ -342,22 +342,22 @@ class TestHTTPS(tservers.HTTPProxyTest, CommonMixin, TcpMixin):
|
||||
|
||||
def test_clientcert_file(self):
|
||||
try:
|
||||
self.config.clientcerts = os.path.join(
|
||||
self.config.client_certs = os.path.join(
|
||||
tutils.test_data.path("mitmproxy/data/clientcert"), "client.pem")
|
||||
f = self.pathod("304")
|
||||
assert f.status_code == 304
|
||||
assert self.server.last_log()["request"]["clientcert"]["keyinfo"]
|
||||
finally:
|
||||
self.config.clientcerts = None
|
||||
self.config.client_certs = None
|
||||
|
||||
def test_clientcert_dir(self):
|
||||
try:
|
||||
self.config.clientcerts = tutils.test_data.path("mitmproxy/data/clientcert")
|
||||
self.config.client_certs = tutils.test_data.path("mitmproxy/data/clientcert")
|
||||
f = self.pathod("304")
|
||||
assert f.status_code == 304
|
||||
assert self.server.last_log()["request"]["clientcert"]["keyinfo"]
|
||||
finally:
|
||||
self.config.clientcerts = None
|
||||
self.config.client_certs = None
|
||||
|
||||
def test_error_post_connect(self):
|
||||
p = self.pathoc()
|
||||
|
@ -96,12 +96,12 @@ class TestProcessProxyOptions:
|
||||
|
||||
def test_upstream_trusted_cadir(self):
|
||||
expected_dir = "/path/to/a/ca/dir"
|
||||
p = self.assert_noerr("--upstream-trusted-cadir", expected_dir)
|
||||
p = self.assert_noerr("--ssl-verify-upstream-trusted-cadir", expected_dir)
|
||||
assert p.options.ssl_verify_upstream_trusted_cadir == expected_dir
|
||||
|
||||
def test_upstream_trusted_ca(self):
|
||||
expected_file = "/path/to/a/cert/file"
|
||||
p = self.assert_noerr("--upstream-trusted-ca", expected_file)
|
||||
p = self.assert_noerr("--ssl-verify-upstream-trusted-ca", expected_file)
|
||||
assert p.options.ssl_verify_upstream_trusted_ca == expected_file
|
||||
|
||||
|
||||
|
@ -11,8 +11,8 @@ def test_common():
|
||||
|
||||
assert cmdline.get_common_options(args)
|
||||
|
||||
args.stickycookie_filt = "foo"
|
||||
args.stickyauth_filt = "foo"
|
||||
args.stickycookie = "foo"
|
||||
args.stickyauth = "foo"
|
||||
v = cmdline.get_common_options(args)
|
||||
assert v["stickycookie"] == "foo"
|
||||
assert v["stickyauth"] == "foo"
|
||||
|
Loading…
Reference in New Issue
Block a user