forked from coolaj86/goldilocks.js
		
	remoteAddress priority... and whitespace... ooops
This commit is contained in:
		
							parent
							
								
									4b470ffe51
								
							
						
					
					
						commit
						350d87c38d
					
				@ -7,76 +7,76 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
  var PromiseA = require('bluebird');
 | 
			
		||||
  var greenlock = require('greenlock');
 | 
			
		||||
  var listeners = require('./servers').listeners;
 | 
			
		||||
	var parseSni = require('sni');
 | 
			
		||||
	var modules = { };
 | 
			
		||||
	var program = {
 | 
			
		||||
		tlsOptions: require('localhost.daplie.me-certificates').merge({})
 | 
			
		||||
	, acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory'
 | 
			
		||||
	, challengeType: 'tls-sni-01'
 | 
			
		||||
	};
 | 
			
		||||
  var parseSni = require('sni');
 | 
			
		||||
  var modules = { };
 | 
			
		||||
  var program = {
 | 
			
		||||
    tlsOptions: require('localhost.daplie.me-certificates').merge({})
 | 
			
		||||
  , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory'
 | 
			
		||||
  , challengeType: 'tls-sni-01'
 | 
			
		||||
  };
 | 
			
		||||
  var secureContexts = {};
 | 
			
		||||
	var tunnelAdminTlsOpts = {};
 | 
			
		||||
	var tls = require('tls');
 | 
			
		||||
  var tunnelAdminTlsOpts = {};
 | 
			
		||||
  var tls = require('tls');
 | 
			
		||||
 | 
			
		||||
	var tcpRouter = {
 | 
			
		||||
		_map: { }
 | 
			
		||||
	, _create: function (address, port) {
 | 
			
		||||
			// port provides hinting for http, smtp, etc
 | 
			
		||||
			return function (conn, firstChunk, opts) {
 | 
			
		||||
				console.log('[tcpRouter] ' + address + ':' + port + ' ' + (opts.servername || ''));
 | 
			
		||||
  var tcpRouter = {
 | 
			
		||||
    _map: { }
 | 
			
		||||
  , _create: function (address, port) {
 | 
			
		||||
      // port provides hinting for http, smtp, etc
 | 
			
		||||
      return function (conn, firstChunk, opts) {
 | 
			
		||||
        console.log('[tcpRouter] ' + address + ':' + port + ' ' + (opts.servername || ''));
 | 
			
		||||
 | 
			
		||||
				var m;
 | 
			
		||||
				var str;
 | 
			
		||||
        var m;
 | 
			
		||||
        var str;
 | 
			
		||||
        var hostname;
 | 
			
		||||
        var newHeads;
 | 
			
		||||
 | 
			
		||||
				// TODO test per-module
 | 
			
		||||
				// Maybe HTTP
 | 
			
		||||
				if (firstChunk[0] > 32 && firstChunk[0] < 127) {
 | 
			
		||||
					str = firstChunk.toString();
 | 
			
		||||
					m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
 | 
			
		||||
        // TODO test per-module
 | 
			
		||||
        // Maybe HTTP
 | 
			
		||||
        if (firstChunk[0] > 32 && firstChunk[0] < 127) {
 | 
			
		||||
          str = firstChunk.toString();
 | 
			
		||||
          m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
 | 
			
		||||
          hostname = (m && m[1].toLowerCase() || '').split(':')[0];
 | 
			
		||||
					console.log('[tcpRouter] hostname', hostname);
 | 
			
		||||
					if (/HTTP\//i.test(str)) {
 | 
			
		||||
						//conn.__service = 'http';
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
          console.log('[tcpRouter] hostname', hostname);
 | 
			
		||||
          if (/HTTP\//i.test(str)) {
 | 
			
		||||
            //conn.__service = 'http';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
				if (!hostname) {
 | 
			
		||||
					// TODO allow tcp tunneling
 | 
			
		||||
					// TODO we need some way of tagging tcp as either terminated tls or insecure
 | 
			
		||||
					conn.write(
 | 
			
		||||
						"HTTP/1.1 404 Not Found\r\n"
 | 
			
		||||
					+ "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
 | 
			
		||||
					+ "Content-Type: text/html\r\n"
 | 
			
		||||
					+ "Content-Length: " + 9 + "\r\n"
 | 
			
		||||
					+ "\r\n"
 | 
			
		||||
					+ "Not Found"
 | 
			
		||||
					);
 | 
			
		||||
        if (!hostname) {
 | 
			
		||||
          // TODO allow tcp tunneling
 | 
			
		||||
          // TODO we need some way of tagging tcp as either terminated tls or insecure
 | 
			
		||||
          conn.write(
 | 
			
		||||
            "HTTP/1.1 404 Not Found\r\n"
 | 
			
		||||
          + "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
 | 
			
		||||
          + "Content-Type: text/html\r\n"
 | 
			
		||||
          + "Content-Length: " + 9 + "\r\n"
 | 
			
		||||
          + "\r\n"
 | 
			
		||||
          + "Not Found"
 | 
			
		||||
          );
 | 
			
		||||
          conn.end();
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // Poor-man's http proxy
 | 
			
		||||
        // XXX SECURITY XXX: should strip existing X-Forwarded headers
 | 
			
		||||
        newHeads =
 | 
			
		||||
          [ "X-Forwarded-Proto: " + (opts.encrypted ? 'https' : 'http')
 | 
			
		||||
          , "X-Forwarded-For: " + (conn.remoteAddress || opts.remoteAddress)
 | 
			
		||||
          , "X-Forwarded-For: " + (opts.remoteAddress || conn.remoteAddress)
 | 
			
		||||
          , "X-Forwarded-Host: " + hostname
 | 
			
		||||
          ];
 | 
			
		||||
 | 
			
		||||
				if (!opts.encrypted) {
 | 
			
		||||
        if (!opts.encrypted) {
 | 
			
		||||
          // a exists-only header that a bad client could not remove
 | 
			
		||||
          newHeads.push("X-Not-Encrypted: yes");
 | 
			
		||||
        }
 | 
			
		||||
				if (opts.servername) {
 | 
			
		||||
        if (opts.servername) {
 | 
			
		||||
          newHeads.push("X-Forwarded-Sni: " + opts.servername);
 | 
			
		||||
          if (opts.servername !== hostname) {
 | 
			
		||||
            // an exists-only header that a bad client could not remove
 | 
			
		||||
            newHeads.push("X-Two-Servernames: yes");
 | 
			
		||||
          }
 | 
			
		||||
				}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        firstChunk = firstChunk.toString('utf8');
 | 
			
		||||
        // JSON.stringify("Host: example.com\r\nNext: Header".replace(/(Host: [^\r\n]*)/i, "$1" + "\r\n" + "X: XYZ"))
 | 
			
		||||
@ -87,16 +87,16 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
 | 
			
		||||
        //
 | 
			
		||||
        // hard-coded routes for the admin interface
 | 
			
		||||
				if (
 | 
			
		||||
        if (
 | 
			
		||||
          /\blocalhost\.admin\./.test(hostname) || /\badmin\.localhost\./.test(hostname)
 | 
			
		||||
          || /\blocalhost\.alpha\./.test(hostname) || /\balpha\.localhost\./.test(hostname)
 | 
			
		||||
        ) {
 | 
			
		||||
					if (!modules.admin) {
 | 
			
		||||
						modules.admin = require('./modules/admin.js').create(deps, config);
 | 
			
		||||
					}
 | 
			
		||||
					modules.admin.emit('connection', conn);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
          if (!modules.admin) {
 | 
			
		||||
            modules.admin = require('./modules/admin.js').create(deps, config);
 | 
			
		||||
          }
 | 
			
		||||
          modules.admin.emit('connection', conn);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // TODO static file handiling and such or whatever
 | 
			
		||||
        if (!modules.http) {
 | 
			
		||||
@ -105,35 +105,37 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
        opts.hostname = hostname;
 | 
			
		||||
        conn.__opts = opts;
 | 
			
		||||
        modules.http.emit('connection', conn);
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	, get: function getTcpRouter(address, port) {
 | 
			
		||||
			address = address || '0.0.0.0';
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  , get: function getTcpRouter(address, port) {
 | 
			
		||||
      address = address || '0.0.0.0';
 | 
			
		||||
 | 
			
		||||
			var id = address + ':' + port;
 | 
			
		||||
			if (!tcpRouter._map[id]) {
 | 
			
		||||
				tcpRouter._map[id] = tcpRouter._create(address, port);
 | 
			
		||||
			}
 | 
			
		||||
      var id = address + ':' + port;
 | 
			
		||||
      if (!tcpRouter._map[id]) {
 | 
			
		||||
        tcpRouter._map[id] = tcpRouter._create(address, port);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
			return tcpRouter._map[id];
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
	var tlsRouter = {
 | 
			
		||||
		_map: { }
 | 
			
		||||
	, _create: function (address, port/*, nextServer*/) {
 | 
			
		||||
			// port provides hinting for https, smtps, etc
 | 
			
		||||
			return function (socket, firstChunk, opts) {
 | 
			
		||||
      return tcpRouter._map[id];
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  var tlsRouter = {
 | 
			
		||||
    _map: { }
 | 
			
		||||
  , _create: function (address, port/*, nextServer*/) {
 | 
			
		||||
      // port provides hinting for https, smtps, etc
 | 
			
		||||
      return function (socket, firstChunk, opts) {
 | 
			
		||||
        var servername = opts.servername;
 | 
			
		||||
				var packerStream = require('tunnel-packer').Stream;
 | 
			
		||||
				var myDuplex = packerStream.create(socket);
 | 
			
		||||
        var packerStream = require('tunnel-packer').Stream;
 | 
			
		||||
        var myDuplex = packerStream.create(socket);
 | 
			
		||||
 | 
			
		||||
				console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress);
 | 
			
		||||
        myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress;
 | 
			
		||||
        myDuplex.remotePort = opts.remotePort || myDuplex.remotePort;
 | 
			
		||||
        console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress);
 | 
			
		||||
 | 
			
		||||
				// needs to wind up in one of 3 states:
 | 
			
		||||
				// 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
 | 
			
		||||
				// 2. Admin Interface (skips the proxying)
 | 
			
		||||
				// 3. Terminated (goes on to a particular module or route)
 | 
			
		||||
				//myDuplex.__tlsTerminated = true;
 | 
			
		||||
        // needs to wind up in one of 3 states:
 | 
			
		||||
        // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
 | 
			
		||||
        // 2. Admin Interface (skips the proxying)
 | 
			
		||||
        // 3. Terminated (goes on to a particular module or route)
 | 
			
		||||
        //myDuplex.__tlsTerminated = true;
 | 
			
		||||
 | 
			
		||||
        process.nextTick(function () {
 | 
			
		||||
          // this must happen after the socket is emitted to the next in the chain,
 | 
			
		||||
@ -142,36 +144,36 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // nextServer.emit could be used here
 | 
			
		||||
				program.tlsTunnelServer.emit('connection', myDuplex);
 | 
			
		||||
        program.tlsTunnelServer.emit('connection', myDuplex);
 | 
			
		||||
 | 
			
		||||
        // Why all this wacky-do with the myDuplex?
 | 
			
		||||
        // because https://github.com/nodejs/node/issues/8854, that's why
 | 
			
		||||
				// (because node's internal networking layer == 💩  sometimes)
 | 
			
		||||
				socket.on('data', function (chunk) {
 | 
			
		||||
					console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
 | 
			
		||||
					myDuplex.push(chunk);
 | 
			
		||||
				});
 | 
			
		||||
				socket.on('error', function (err) {
 | 
			
		||||
					console.error('[error] httpsTunnel (Admin) TODO close');
 | 
			
		||||
					console.error(err);
 | 
			
		||||
					myDuplex.emit('error', err);
 | 
			
		||||
				});
 | 
			
		||||
				socket.on('close', function () {
 | 
			
		||||
					myDuplex.end();
 | 
			
		||||
				});
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	, get: function getTcpRouter(address, port) {
 | 
			
		||||
			address = address || '0.0.0.0';
 | 
			
		||||
        // (because node's internal networking layer == 💩  sometimes)
 | 
			
		||||
        socket.on('data', function (chunk) {
 | 
			
		||||
          console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
 | 
			
		||||
          myDuplex.push(chunk);
 | 
			
		||||
        });
 | 
			
		||||
        socket.on('error', function (err) {
 | 
			
		||||
          console.error('[error] httpsTunnel (Admin) TODO close');
 | 
			
		||||
          console.error(err);
 | 
			
		||||
          myDuplex.emit('error', err);
 | 
			
		||||
        });
 | 
			
		||||
        socket.on('close', function () {
 | 
			
		||||
          myDuplex.end();
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  , get: function getTcpRouter(address, port) {
 | 
			
		||||
      address = address || '0.0.0.0';
 | 
			
		||||
 | 
			
		||||
			var id = address + ':' + port;
 | 
			
		||||
			if (!tlsRouter._map[id]) {
 | 
			
		||||
				tlsRouter._map[id] = tlsRouter._create(address, port);
 | 
			
		||||
			}
 | 
			
		||||
      var id = address + ':' + port;
 | 
			
		||||
      if (!tlsRouter._map[id]) {
 | 
			
		||||
        tlsRouter._map[id] = tlsRouter._create(address, port);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
			return tlsRouter._map[id];
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
      return tlsRouter._map[id];
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  // opts = { servername, encrypted, remoteAddress, remotePort }
 | 
			
		||||
@ -184,77 +186,77 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
 | 
			
		||||
      // TODO port-based routing can do here
 | 
			
		||||
 | 
			
		||||
			// TLS
 | 
			
		||||
			if (22 === firstChunk[0]) {
 | 
			
		||||
      // TLS
 | 
			
		||||
      if (22 === firstChunk[0]) {
 | 
			
		||||
        servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid';
 | 
			
		||||
        console.log('tryTls');
 | 
			
		||||
				tlsRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts);
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
        tlsRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        console.log('tryTcp');
 | 
			
		||||
				tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts);
 | 
			
		||||
			}
 | 
			
		||||
        tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	function approveDomains(opts, certs, cb) {
 | 
			
		||||
		// This is where you check your database and associated
 | 
			
		||||
		// email addresses with domains and agreements and such
 | 
			
		||||
  function approveDomains(opts, certs, cb) {
 | 
			
		||||
    // This is where you check your database and associated
 | 
			
		||||
    // email addresses with domains and agreements and such
 | 
			
		||||
 | 
			
		||||
		// The domains being approved for the first time are listed in opts.domains
 | 
			
		||||
		// Certs being renewed are listed in certs.altnames
 | 
			
		||||
    // The domains being approved for the first time are listed in opts.domains
 | 
			
		||||
    // Certs being renewed are listed in certs.altnames
 | 
			
		||||
 | 
			
		||||
		function complete(err, stuff) {
 | 
			
		||||
			opts.email = stuff.email;
 | 
			
		||||
			opts.agreeTos = stuff.agreeTos;
 | 
			
		||||
			opts.server = stuff.server;
 | 
			
		||||
			opts.challengeType = stuff.challengeType;
 | 
			
		||||
    function complete(err, stuff) {
 | 
			
		||||
      opts.email = stuff.email;
 | 
			
		||||
      opts.agreeTos = stuff.agreeTos;
 | 
			
		||||
      opts.server = stuff.server;
 | 
			
		||||
      opts.challengeType = stuff.challengeType;
 | 
			
		||||
 | 
			
		||||
			cb(null, { options: opts, certs: certs });
 | 
			
		||||
		}
 | 
			
		||||
      cb(null, { options: opts, certs: certs });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		if (certs) {
 | 
			
		||||
			// TODO make sure the same options are used for renewal as for registration?
 | 
			
		||||
			opts.domains = certs.altnames;
 | 
			
		||||
    if (certs) {
 | 
			
		||||
      // TODO make sure the same options are used for renewal as for registration?
 | 
			
		||||
      opts.domains = certs.altnames;
 | 
			
		||||
 | 
			
		||||
			cb(null, { options: opts, certs: certs });
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
      cb(null, { options: opts, certs: certs });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		// check config for domain name
 | 
			
		||||
		if (-1 !== config.tls.servernames.indexOf(opts.domain)) {
 | 
			
		||||
			// TODO how to handle SANs?
 | 
			
		||||
			// TODO fetch domain-specific email
 | 
			
		||||
			// TODO fetch domain-specific acmeDirectory
 | 
			
		||||
			// NOTE: you can also change other options such as `challengeType` and `challenge`
 | 
			
		||||
			// opts.challengeType = 'http-01';
 | 
			
		||||
			// opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet
 | 
			
		||||
			complete(null, {
 | 
			
		||||
				email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType });
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		// TODO ask http module about the default path (/srv/www/:hostname)
 | 
			
		||||
		// (if it exists, we can allow and add to config)
 | 
			
		||||
		if (!modules.http) {
 | 
			
		||||
			modules.http = require('./modules/http.js').create(config);
 | 
			
		||||
		}
 | 
			
		||||
		modules.http.checkServername(opts.domain).then(function (stuff) {
 | 
			
		||||
			if (!stuff.domains) {
 | 
			
		||||
				// TODO once precheck is implemented we can just let it pass if it passes, yknow?
 | 
			
		||||
				cb(new Error('domain is not allowed'));
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
    // check config for domain name
 | 
			
		||||
    if (-1 !== config.tls.servernames.indexOf(opts.domain)) {
 | 
			
		||||
      // TODO how to handle SANs?
 | 
			
		||||
      // TODO fetch domain-specific email
 | 
			
		||||
      // TODO fetch domain-specific acmeDirectory
 | 
			
		||||
      // NOTE: you can also change other options such as `challengeType` and `challenge`
 | 
			
		||||
      // opts.challengeType = 'http-01';
 | 
			
		||||
      // opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet
 | 
			
		||||
      complete(null, {
 | 
			
		||||
        email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO ask http module about the default path (/srv/www/:hostname)
 | 
			
		||||
    // (if it exists, we can allow and add to config)
 | 
			
		||||
    if (!modules.http) {
 | 
			
		||||
      modules.http = require('./modules/http.js').create(config);
 | 
			
		||||
    }
 | 
			
		||||
    modules.http.checkServername(opts.domain).then(function (stuff) {
 | 
			
		||||
      if (!stuff.domains) {
 | 
			
		||||
        // TODO once precheck is implemented we can just let it pass if it passes, yknow?
 | 
			
		||||
        cb(new Error('domain is not allowed'));
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
			complete(null, {
 | 
			
		||||
				domain: stuff.domain || stuff.domains[0]
 | 
			
		||||
			, domains: stuff.domains
 | 
			
		||||
			, email: program.email
 | 
			
		||||
			, server: program.acmeDirectoryUrl
 | 
			
		||||
			, challengeType: program.challengeType
 | 
			
		||||
			});
 | 
			
		||||
			return;
 | 
			
		||||
		}, cb);
 | 
			
		||||
	}
 | 
			
		||||
      complete(null, {
 | 
			
		||||
        domain: stuff.domain || stuff.domains[0]
 | 
			
		||||
      , domains: stuff.domains
 | 
			
		||||
      , email: program.email
 | 
			
		||||
      , server: program.acmeDirectoryUrl
 | 
			
		||||
      , challengeType: program.challengeType
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }, cb);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getAcme() {
 | 
			
		||||
    return greenlock.create({
 | 
			
		||||
@ -319,7 +321,7 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
    //  console.log('terminated data:', chunk.toString());
 | 
			
		||||
    //});
 | 
			
		||||
    //(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
 | 
			
		||||
		//tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { encrypted: false });
 | 
			
		||||
    //tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { encrypted: false });
 | 
			
		||||
    handler(tlsSocket, {
 | 
			
		||||
      servername: tlsSocket.servername
 | 
			
		||||
    , encrypted: true
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user