forked from root/acme.js
		
	request cleanup
This commit is contained in:
		
							parent
							
								
									54cda5a888
								
							
						
					
					
						commit
						b39a3763cf
					
				
							
								
								
									
										30
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								README.md
									
									
									
									
									
								
							@ -1,4 +1,4 @@
 | 
				
			|||||||
# [ACME.js](https://git.rootprojects.org/root/acme.js) v3
 | 
					# [ACME.js](https://git.rootprojects.org/root/acme.js) (RFC 8555 / November 2019)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
 | 
					| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -52,6 +52,31 @@ If they don't, please open an issue to let us know why.
 | 
				
			|||||||
We'd much rather improve the app than have a hundred different versions running in the wild.
 | 
					We'd much rather improve the app than have a hundred different versions running in the wild.
 | 
				
			||||||
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
 | 
					However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# API Overview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					ACME.create({ maintainerEmail, packageAgent });
 | 
				
			||||||
 | 
					acme.init(directoryUrl);
 | 
				
			||||||
 | 
					acme.accounts.create({ subscriberEmail, agreeToTerms, accountKey });
 | 
				
			||||||
 | 
					acme.certificates.create({
 | 
				
			||||||
 | 
						customerEmail, // do not use
 | 
				
			||||||
 | 
						account,
 | 
				
			||||||
 | 
						accountKey,
 | 
				
			||||||
 | 
						serverKey,
 | 
				
			||||||
 | 
						csr,
 | 
				
			||||||
 | 
						domains,
 | 
				
			||||||
 | 
						challenges
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					ACME.computeChallenge({
 | 
				
			||||||
 | 
						accountKey: jwk,
 | 
				
			||||||
 | 
						hostname: 'example.com',
 | 
				
			||||||
 | 
						challenge: { type: 'dns-01', token: 'xxxx' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install
 | 
					# Install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To make it easy to generate, encode, and decode keys and certificates,
 | 
					To make it easy to generate, encode, and decode keys and certificates,
 | 
				
			||||||
@ -234,9 +259,6 @@ is a required part of the process, which requires `set` and `remove` callbacks/p
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```js
 | 
					```js
 | 
				
			||||||
var certinfo = await acme.certificates.create({
 | 
					var certinfo = await acme.certificates.create({
 | 
				
			||||||
	agreeToTerms: function(tos) {
 | 
					 | 
				
			||||||
		return tos;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	account: account,
 | 
						account: account,
 | 
				
			||||||
	accountKey: accountPrivateJwk,
 | 
						accountKey: accountPrivateJwk,
 | 
				
			||||||
	csr: csr,
 | 
						csr: csr,
 | 
				
			||||||
 | 
				
			|||||||
@ -18,33 +18,37 @@ native._canCheck = function(me) {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
native._dns01 = function(me, ch) {
 | 
					native._dns01 = function(me, ch) {
 | 
				
			||||||
	return new me.request({
 | 
						return me
 | 
				
			||||||
		url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
 | 
							.request({
 | 
				
			||||||
	}).then(function(resp) {
 | 
								url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
 | 
				
			||||||
		var err;
 | 
							})
 | 
				
			||||||
		if (!resp.body || !Array.isArray(resp.body.answer)) {
 | 
							.then(function(resp) {
 | 
				
			||||||
			err = new Error('failed to get DNS response');
 | 
								var err;
 | 
				
			||||||
			console.error(err);
 | 
								if (!resp.body || !Array.isArray(resp.body.answer)) {
 | 
				
			||||||
			throw err;
 | 
									err = new Error('failed to get DNS response');
 | 
				
			||||||
		}
 | 
									console.error(err);
 | 
				
			||||||
		if (!resp.body.answer.length) {
 | 
									throw err;
 | 
				
			||||||
			err = new Error('failed to get DNS answer record in response');
 | 
								}
 | 
				
			||||||
			console.error(err);
 | 
								if (!resp.body.answer.length) {
 | 
				
			||||||
			throw err;
 | 
									err = new Error('failed to get DNS answer record in response');
 | 
				
			||||||
		}
 | 
									console.error(err);
 | 
				
			||||||
		return {
 | 
									throw err;
 | 
				
			||||||
			answer: resp.body.answer.map(function(ans) {
 | 
								}
 | 
				
			||||||
				return { data: ans.data, ttl: ans.ttl };
 | 
								return {
 | 
				
			||||||
			})
 | 
									answer: resp.body.answer.map(function(ans) {
 | 
				
			||||||
		};
 | 
										return { data: ans.data, ttl: ans.ttl };
 | 
				
			||||||
	});
 | 
									})
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
native._http01 = function(me, ch) {
 | 
					native._http01 = function(me, ch) {
 | 
				
			||||||
	var url = encodeURIComponent(ch.challengeUrl);
 | 
						var url = encodeURIComponent(ch.challengeUrl);
 | 
				
			||||||
	return new me.request({
 | 
						return me
 | 
				
			||||||
		url: me._baseUrl + '/api/http?url=' + url
 | 
							.request({
 | 
				
			||||||
	}).then(function(resp) {
 | 
								url: me._baseUrl + '/api/http?url=' + url
 | 
				
			||||||
		return resp.body;
 | 
							})
 | 
				
			||||||
	});
 | 
							.then(function(resp) {
 | 
				
			||||||
 | 
								return resp.body;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
'use strict';
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var UserAgent = module.exports;
 | 
					var UserAgent = module.exports;
 | 
				
			||||||
UserAgent.get = function () {
 | 
					UserAgent.get = function() {
 | 
				
			||||||
  return false;
 | 
						return false;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@
 | 
				
			|||||||
var native = module.exports;
 | 
					var native = module.exports;
 | 
				
			||||||
var promisify = require('util').promisify;
 | 
					var promisify = require('util').promisify;
 | 
				
			||||||
var resolveTxt = promisify(require('dns').resolveTxt);
 | 
					var resolveTxt = promisify(require('dns').resolveTxt);
 | 
				
			||||||
 | 
					var crypto = require('crypto');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
native._canCheck = function(me) {
 | 
					native._canCheck = function(me) {
 | 
				
			||||||
	me._canCheck = {};
 | 
						me._canCheck = {};
 | 
				
			||||||
@ -31,3 +32,57 @@ native._http01 = function(me, ch) {
 | 
				
			|||||||
		return resp.body;
 | 
							return resp.body;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// the hashcash here is for browser parity only
 | 
				
			||||||
 | 
					// basically we ask the client to find a needle in a haystack
 | 
				
			||||||
 | 
					// (very similar to CloudFlare's api protection)
 | 
				
			||||||
 | 
					native._hashcash = function(ch) {
 | 
				
			||||||
 | 
						if (!ch || !ch.nonce) {
 | 
				
			||||||
 | 
							ch = { nonce: 'xxx' };
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Promise.resolve()
 | 
				
			||||||
 | 
							.then(function() {
 | 
				
			||||||
 | 
								// only get easy answers
 | 
				
			||||||
 | 
								var len = ch.needle.length;
 | 
				
			||||||
 | 
								var start = ch.start || 0;
 | 
				
			||||||
 | 
								var end = ch.end || Math.ceil(len / 2);
 | 
				
			||||||
 | 
								var window = parseInt(end - start, 10) || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var maxLen = 6;
 | 
				
			||||||
 | 
								var maxTries = Math.pow(2, maxLen * 8);
 | 
				
			||||||
 | 
								if (
 | 
				
			||||||
 | 
									len > maxLen ||
 | 
				
			||||||
 | 
									window < Math.ceil(len / 2) ||
 | 
				
			||||||
 | 
									ch.needle.toLowerCase() !== ch.needle ||
 | 
				
			||||||
 | 
									ch.alg !== 'SHA-256'
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
 | 
									// bail unless the server is issuing very easy challenges
 | 
				
			||||||
 | 
									throw new Error('possible and easy answers only, please');
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var haystack;
 | 
				
			||||||
 | 
								var i;
 | 
				
			||||||
 | 
								var answer;
 | 
				
			||||||
 | 
								var needle = Buffer.from(ch.needle, 'hex');
 | 
				
			||||||
 | 
								for (i = 0; i < maxTries; i += 1) {
 | 
				
			||||||
 | 
									answer = i.toString(16);
 | 
				
			||||||
 | 
									if (answer.length % 2) {
 | 
				
			||||||
 | 
										answer = '0' + answer;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									haystack = crypto
 | 
				
			||||||
 | 
										.createHash('sha256')
 | 
				
			||||||
 | 
										.update(Buffer.from(ch.nonce + answer, 'hex'))
 | 
				
			||||||
 | 
										.digest()
 | 
				
			||||||
 | 
										.slice(ch.start, ch.end);
 | 
				
			||||||
 | 
									if (-1 !== haystack.indexOf(needle)) {
 | 
				
			||||||
 | 
										return ch.nonce + ':' + answer;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return ch.nonce + ':xxx';
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							.catch(function() {
 | 
				
			||||||
 | 
								//console.log('[debug]', err);
 | 
				
			||||||
 | 
								// ignore any error
 | 
				
			||||||
 | 
								return ch.nonce + ':xxx';
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
'use strict';
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var os = require('os');
 | 
					var os = require('os');
 | 
				
			||||||
var ver = require('../../package.json');
 | 
					var ver = require('../../package.json').version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var UserAgent = module.exports;
 | 
					var UserAgent = module.exports;
 | 
				
			||||||
UserAgent.get = function(me) {
 | 
					UserAgent.get = function(me) {
 | 
				
			||||||
 | 
				
			|||||||
@ -5,15 +5,5 @@ var promisify = require('util').promisify;
 | 
				
			|||||||
var request = promisify(require('@root/request'));
 | 
					var request = promisify(require('@root/request'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
http.request = function(opts) {
 | 
					http.request = function(opts) {
 | 
				
			||||||
	if (!opts.headers) {
 | 
					 | 
				
			||||||
		opts.headers = {};
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (
 | 
					 | 
				
			||||||
		!Object.keys(opts.headers).some(function(key) {
 | 
					 | 
				
			||||||
			return 'user-agent' === key.toLowerCase();
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	) {
 | 
					 | 
				
			||||||
		// TODO opts.headers['User-Agent'] = 'TODO';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return request(opts);
 | 
						return request(opts);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require('dotenv').config();
 | 
					require('dotenv').config();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var pkg = require('../package.json');
 | 
				
			||||||
var CSR = require('@root/csr');
 | 
					var CSR = require('@root/csr');
 | 
				
			||||||
var Enc = require('@root/encoding/base64');
 | 
					var Enc = require('@root/encoding/base64');
 | 
				
			||||||
var PEM = require('@root/pem');
 | 
					var PEM = require('@root/pem');
 | 
				
			||||||
var punycode = require('punycode');
 | 
					var punycode = require('punycode');
 | 
				
			||||||
var ACME = require('../acme.js');
 | 
					var ACME = require('../acme.js');
 | 
				
			||||||
var Keypairs = require('@root/keypairs');
 | 
					var Keypairs = require('@root/keypairs');
 | 
				
			||||||
 | 
					var ecJwk = require('../fixtures/account.jwk.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO exec npm install --save-dev CHALLENGE_MODULE
 | 
					// TODO exec npm install --save-dev CHALLENGE_MODULE
 | 
				
			||||||
if (!process.env.CHALLENGE_OPTIONS) {
 | 
					if (!process.env.CHALLENGE_OPTIONS) {
 | 
				
			||||||
@ -36,6 +38,7 @@ module.exports = function() {
 | 
				
			|||||||
	var acme = ACME.create({
 | 
						var acme = ACME.create({
 | 
				
			||||||
		// debug: true
 | 
							// debug: true
 | 
				
			||||||
		maintainerEmail: config.email,
 | 
							maintainerEmail: config.email,
 | 
				
			||||||
 | 
							packageAgent: 'test-' + pkg.name + '/' + pkg.version,
 | 
				
			||||||
		notify: function(ev, params) {
 | 
							notify: function(ev, params) {
 | 
				
			||||||
			console.info(
 | 
								console.info(
 | 
				
			||||||
				'\t' + ev,
 | 
									'\t' + ev,
 | 
				
			||||||
@ -104,6 +107,10 @@ module.exports = function() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var accountKeypair = await Keypairs.generate({ kty: accKty });
 | 
							var accountKeypair = await Keypairs.generate({ kty: accKty });
 | 
				
			||||||
 | 
							if (/EC/i.test(accKty)) {
 | 
				
			||||||
 | 
								// to test that an existing account gets back data
 | 
				
			||||||
 | 
								accountKeypair = ecJwk;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		var accountKey = accountKeypair.private;
 | 
							var accountKey = accountKeypair.private;
 | 
				
			||||||
		if (config.debug) {
 | 
							if (config.debug) {
 | 
				
			||||||
			console.info('Account Key Created');
 | 
								console.info('Account Key Created');
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										74
									
								
								tests/maintainer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								tests/maintainer.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var native = require('../lib/native.js');
 | 
				
			||||||
 | 
					var crypto = require('crypto');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					native
 | 
				
			||||||
 | 
						._hashcash({
 | 
				
			||||||
 | 
							alg: 'SHA-256',
 | 
				
			||||||
 | 
							nonce: '00',
 | 
				
			||||||
 | 
							needle: '0000',
 | 
				
			||||||
 | 
							start: 0,
 | 
				
			||||||
 | 
							end: 2
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						.then(function(hashcash) {
 | 
				
			||||||
 | 
							if ('00:76de' !== hashcash) {
 | 
				
			||||||
 | 
								throw new Error('hashcash algorthim changed');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							console.info('PASS: known hash solves correctly');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return native
 | 
				
			||||||
 | 
								._hashcash({
 | 
				
			||||||
 | 
									alg: 'SHA-256',
 | 
				
			||||||
 | 
									nonce: '10',
 | 
				
			||||||
 | 
									needle: '',
 | 
				
			||||||
 | 
									start: 0,
 | 
				
			||||||
 | 
									end: 2
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.then(function(hashcash) {
 | 
				
			||||||
 | 
									if ('10:00' !== hashcash) {
 | 
				
			||||||
 | 
										throw new Error('hashcash algorthim changed');
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									console.info('PASS: empty hash solves correctly');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									var now = Date.now();
 | 
				
			||||||
 | 
									var nonce = '20';
 | 
				
			||||||
 | 
									var needle = crypto
 | 
				
			||||||
 | 
										.randomBytes(3)
 | 
				
			||||||
 | 
										.toString('hex')
 | 
				
			||||||
 | 
										.slice(0, 5);
 | 
				
			||||||
 | 
									native
 | 
				
			||||||
 | 
										._hashcash({
 | 
				
			||||||
 | 
											alg: 'SHA-256',
 | 
				
			||||||
 | 
											nonce: nonce,
 | 
				
			||||||
 | 
											needle: needle,
 | 
				
			||||||
 | 
											start: 0,
 | 
				
			||||||
 | 
											end: Math.ceil(needle.length / 2)
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
										.then(function(hashcash) {
 | 
				
			||||||
 | 
											var later = Date.now();
 | 
				
			||||||
 | 
											var parts = hashcash.split(':');
 | 
				
			||||||
 | 
											var answer = parts[1];
 | 
				
			||||||
 | 
											if (parts[0] !== nonce) {
 | 
				
			||||||
 | 
												throw new Error('incorrect nonce');
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											var haystack = crypto
 | 
				
			||||||
 | 
												.createHash('sha256')
 | 
				
			||||||
 | 
												.update(Buffer.from(nonce + answer, 'hex'))
 | 
				
			||||||
 | 
												.digest()
 | 
				
			||||||
 | 
												.slice(0, Math.ceil(needle.length / 2));
 | 
				
			||||||
 | 
											if (
 | 
				
			||||||
 | 
												-1 === haystack.indexOf(Buffer.from(needle, 'hex'))
 | 
				
			||||||
 | 
											) {
 | 
				
			||||||
 | 
												throw new Error('incorrect solution');
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											if (later - now > 2000) {
 | 
				
			||||||
 | 
												throw new Error('took too long to solve');
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											console.info(
 | 
				
			||||||
 | 
												'PASS: rando hash solves correctly (and in good time - %dms)',
 | 
				
			||||||
 | 
												later - now
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
							
								
								
									
										19
									
								
								utils.js
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								utils.js
									
									
									
									
									
								
							@ -82,9 +82,14 @@ U._request = function(me, opts) {
 | 
				
			|||||||
	if (ua && !opts.headers['User-Agent']) {
 | 
						if (ua && !opts.headers['User-Agent']) {
 | 
				
			||||||
		opts.headers['User-Agent'] = ua;
 | 
							opts.headers['User-Agent'] = ua;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (opts.json && true !== opts.json) {
 | 
						if (opts.json) {
 | 
				
			||||||
		opts.headers['Content-Type'] = 'application/jose+json';
 | 
							opts.headers.Accept = 'application/json';
 | 
				
			||||||
		opts.body = JSON.stringify(opts.json);
 | 
							if (true !== opts.json) {
 | 
				
			||||||
 | 
								opts.body = JSON.stringify(opts.json);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (/*opts.jose ||*/ opts.json.protected) {
 | 
				
			||||||
 | 
								opts.headers['Content-Type'] = 'application/jose+json';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (!opts.method) {
 | 
						if (!opts.method) {
 | 
				
			||||||
		opts.method = 'GET';
 | 
							opts.method = 'GET';
 | 
				
			||||||
@ -92,16 +97,10 @@ U._request = function(me, opts) {
 | 
				
			|||||||
			opts.method = 'POST';
 | 
								opts.method = 'POST';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (opts.json) {
 | 
					 | 
				
			||||||
		opts.headers.Accept = 'application/json';
 | 
					 | 
				
			||||||
		if (true !== opts.json) {
 | 
					 | 
				
			||||||
			opts.body = JSON.stringify(opts.json);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//console.log('\n[debug] REQUEST');
 | 
						//console.log('\n[debug] REQUEST');
 | 
				
			||||||
	//console.log(opts);
 | 
						//console.log(opts);
 | 
				
			||||||
	return me.request(opts).then(function(resp) {
 | 
						return me.__request(opts).then(function(resp) {
 | 
				
			||||||
		if (resp.toJSON) {
 | 
							if (resp.toJSON) {
 | 
				
			||||||
			resp = resp.toJSON();
 | 
								resp = resp.toJSON();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user