update
This commit is contained in:
+13
-4
@@ -3,7 +3,7 @@
|
||||
Drop-in replacement for Node's `http` and `https` modules that automatically follows redirects.
|
||||
|
||||
[](https://www.npmjs.com/package/follow-redirects)
|
||||
[](https://travis-ci.org/follow-redirects/follow-redirects)
|
||||
[](https://github.com/follow-redirects/follow-redirects/actions)
|
||||
[](https://coveralls.io/r/follow-redirects/follow-redirects?branch=master)
|
||||
[](https://www.npmjs.com/package/follow-redirects)
|
||||
[](https://github.com/sponsors/RubenVerborgh)
|
||||
@@ -28,13 +28,14 @@ You can inspect the final redirected URL through the `responseUrl` property on t
|
||||
If no redirection happened, `responseUrl` is the original request URL.
|
||||
|
||||
```javascript
|
||||
https.request({
|
||||
const request = https.request({
|
||||
host: 'bitly.com',
|
||||
path: '/UHfDGO',
|
||||
}, response => {
|
||||
console.log(response.responseUrl);
|
||||
// 'http://duckduckgo.com/robots.txt'
|
||||
});
|
||||
request.end();
|
||||
```
|
||||
|
||||
## Options
|
||||
@@ -62,9 +63,17 @@ const { http, https } = require('follow-redirects');
|
||||
|
||||
const options = url.parse('http://bit.ly/900913');
|
||||
options.maxRedirects = 10;
|
||||
options.beforeRedirect = options => {
|
||||
// Use this function to adjust the options upon redirecting,
|
||||
options.beforeRedirect = (options, response, request) => {
|
||||
// Use this to adjust the request options upon redirecting,
|
||||
// to inspect the latest response headers,
|
||||
// or to cancel the request by throwing an error
|
||||
|
||||
// response.headers = the redirect response headers
|
||||
// response.statusCode = the redirect response code (eg. 301, 307, etc.)
|
||||
|
||||
// request.url = the requested URL that resulted in a redirect
|
||||
// request.headers = the headers in the request that resulted in a redirect
|
||||
// request.method = the method of the request that resulted in a redirect
|
||||
if (options.hostname === "example.com") {
|
||||
options.auth = "user:password";
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,9 +1,15 @@
|
||||
var debug;
|
||||
try {
|
||||
/* eslint global-require: off */
|
||||
debug = require("debug")("follow-redirects");
|
||||
}
|
||||
catch (error) {
|
||||
debug = function () { /* */ };
|
||||
}
|
||||
module.exports = debug;
|
||||
|
||||
module.exports = function () {
|
||||
if (!debug) {
|
||||
try {
|
||||
/* eslint global-require: off */
|
||||
debug = require("debug")("follow-redirects");
|
||||
}
|
||||
catch (error) { /* */ }
|
||||
if (typeof debug !== "function") {
|
||||
debug = function () { /* */ };
|
||||
}
|
||||
}
|
||||
debug.apply(null, arguments);
|
||||
};
|
||||
|
||||
+217
-116
@@ -7,8 +7,9 @@ var assert = require("assert");
|
||||
var debug = require("./debug");
|
||||
|
||||
// Create handlers that pass events from native requests
|
||||
var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];
|
||||
var eventHandlers = Object.create(null);
|
||||
["abort", "aborted", "connect", "error", "socket", "timeout"].forEach(function (event) {
|
||||
events.forEach(function (event) {
|
||||
eventHandlers[event] = function (arg1, arg2, arg3) {
|
||||
this._redirectable.emit(event, arg1, arg2, arg3);
|
||||
};
|
||||
@@ -17,7 +18,7 @@ var eventHandlers = Object.create(null);
|
||||
// Error types with codes
|
||||
var RedirectionError = createErrorType(
|
||||
"ERR_FR_REDIRECTION_FAILURE",
|
||||
""
|
||||
"Redirected request failed"
|
||||
);
|
||||
var TooManyRedirectsError = createErrorType(
|
||||
"ERR_FR_TOO_MANY_REDIRECTS",
|
||||
@@ -61,6 +62,11 @@ function RedirectableRequest(options, responseCallback) {
|
||||
}
|
||||
RedirectableRequest.prototype = Object.create(Writable.prototype);
|
||||
|
||||
RedirectableRequest.prototype.abort = function () {
|
||||
abortRequest(this._currentRequest);
|
||||
this.emit("abort");
|
||||
};
|
||||
|
||||
// Writes buffered data to the current native request
|
||||
RedirectableRequest.prototype.write = function (data, encoding, callback) {
|
||||
// Writing is not allowed if end has been called
|
||||
@@ -140,40 +146,72 @@ RedirectableRequest.prototype.removeHeader = function (name) {
|
||||
|
||||
// Global timeout for all underlying requests
|
||||
RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
|
||||
if (callback) {
|
||||
this.once("timeout", callback);
|
||||
var self = this;
|
||||
|
||||
// Destroys the socket on timeout
|
||||
function destroyOnTimeout(socket) {
|
||||
socket.setTimeout(msecs);
|
||||
socket.removeListener("timeout", socket.destroy);
|
||||
socket.addListener("timeout", socket.destroy);
|
||||
}
|
||||
|
||||
// Sets up a timer to trigger a timeout event
|
||||
function startTimer(socket) {
|
||||
if (self._timeout) {
|
||||
clearTimeout(self._timeout);
|
||||
}
|
||||
self._timeout = setTimeout(function () {
|
||||
self.emit("timeout");
|
||||
clearTimer();
|
||||
}, msecs);
|
||||
destroyOnTimeout(socket);
|
||||
}
|
||||
|
||||
// Stops a timeout from triggering
|
||||
function clearTimer() {
|
||||
// Clear the timeout
|
||||
if (self._timeout) {
|
||||
clearTimeout(self._timeout);
|
||||
self._timeout = null;
|
||||
}
|
||||
|
||||
// Clean up all attached listeners
|
||||
self.removeListener("abort", clearTimer);
|
||||
self.removeListener("error", clearTimer);
|
||||
self.removeListener("response", clearTimer);
|
||||
if (callback) {
|
||||
self.removeListener("timeout", callback);
|
||||
}
|
||||
if (!self.socket) {
|
||||
self._currentRequest.removeListener("socket", startTimer);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach callback if passed
|
||||
if (callback) {
|
||||
this.on("timeout", callback);
|
||||
}
|
||||
|
||||
// Start the timer if or when the socket is opened
|
||||
if (this.socket) {
|
||||
startTimer(this, msecs);
|
||||
startTimer(this.socket);
|
||||
}
|
||||
else {
|
||||
var self = this;
|
||||
this._currentRequest.once("socket", function () {
|
||||
startTimer(self, msecs);
|
||||
});
|
||||
this._currentRequest.once("socket", startTimer);
|
||||
}
|
||||
|
||||
this.once("response", clearTimer);
|
||||
this.once("error", clearTimer);
|
||||
// Clean up on events
|
||||
this.on("socket", destroyOnTimeout);
|
||||
this.on("abort", clearTimer);
|
||||
this.on("error", clearTimer);
|
||||
this.on("response", clearTimer);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
function startTimer(request, msecs) {
|
||||
clearTimeout(request._timeout);
|
||||
request._timeout = setTimeout(function () {
|
||||
request.emit("timeout");
|
||||
}, msecs);
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
// Proxy all other public ClientRequest methods
|
||||
[
|
||||
"abort", "flushHeaders", "getHeader",
|
||||
"flushHeaders", "getHeader",
|
||||
"setNoDelay", "setSocketKeepAlive",
|
||||
].forEach(function (method) {
|
||||
RedirectableRequest.prototype[method] = function (a, b) {
|
||||
@@ -232,28 +270,30 @@ RedirectableRequest.prototype._performRequest = function () {
|
||||
// If specified, use the agent corresponding to the protocol
|
||||
// (HTTP and HTTPS use different types of agents)
|
||||
if (this._options.agents) {
|
||||
var scheme = protocol.substr(0, protocol.length - 1);
|
||||
var scheme = protocol.slice(0, -1);
|
||||
this._options.agent = this._options.agents[scheme];
|
||||
}
|
||||
|
||||
// Create the native request
|
||||
// Create the native request and set up its event handlers
|
||||
var request = this._currentRequest =
|
||||
nativeProtocol.request(this._options, this._onNativeResponse);
|
||||
this._currentUrl = url.format(this._options);
|
||||
|
||||
// Set up event handlers
|
||||
request._redirectable = this;
|
||||
for (var event in eventHandlers) {
|
||||
/* istanbul ignore else */
|
||||
if (event) {
|
||||
request.on(event, eventHandlers[event]);
|
||||
}
|
||||
for (var event of events) {
|
||||
request.on(event, eventHandlers[event]);
|
||||
}
|
||||
|
||||
// RFC7230§5.3.1: When making a request directly to an origin server, […]
|
||||
// a client MUST send only the absolute path […] as the request-target.
|
||||
this._currentUrl = /^\//.test(this._options.path) ?
|
||||
url.format(this._options) :
|
||||
// When making a request to a proxy, […]
|
||||
// a client MUST send the target URI in absolute-form […].
|
||||
this._currentUrl = this._options.path;
|
||||
|
||||
// End a redirected request
|
||||
// (The first request must be ended explicitly with RedirectableRequest#end)
|
||||
if (this._isRedirect) {
|
||||
// Write the request entity and end.
|
||||
// Write the request entity and end
|
||||
var i = 0;
|
||||
var self = this;
|
||||
var buffers = this._requestBodyBuffers;
|
||||
@@ -301,85 +341,120 @@ RedirectableRequest.prototype._processResponse = function (response) {
|
||||
// the user agent MAY automatically redirect its request to the URI
|
||||
// referenced by the Location field value,
|
||||
// even if the specific status code is not understood.
|
||||
|
||||
// If the response is not a redirect; return it as-is
|
||||
var location = response.headers.location;
|
||||
if (location && this._options.followRedirects !== false &&
|
||||
statusCode >= 300 && statusCode < 400) {
|
||||
// Abort the current request
|
||||
this._currentRequest.removeAllListeners();
|
||||
this._currentRequest.on("error", noop);
|
||||
this._currentRequest.abort();
|
||||
// Discard the remainder of the response to avoid waiting for data
|
||||
response.destroy();
|
||||
|
||||
// RFC7231§6.4: A client SHOULD detect and intervene
|
||||
// in cyclical redirections (i.e., "infinite" redirection loops).
|
||||
if (++this._redirectCount > this._options.maxRedirects) {
|
||||
this.emit("error", new TooManyRedirectsError());
|
||||
return;
|
||||
}
|
||||
|
||||
// RFC7231§6.4: Automatic redirection needs to done with
|
||||
// care for methods not known to be safe, […]
|
||||
// RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
|
||||
// the request method from POST to GET for the subsequent request.
|
||||
if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
|
||||
// RFC7231§6.4.4: The 303 (See Other) status code indicates that
|
||||
// the server is redirecting the user agent to a different resource […]
|
||||
// A user agent can perform a retrieval request targeting that URI
|
||||
// (a GET or HEAD request if using HTTP) […]
|
||||
(statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
|
||||
this._options.method = "GET";
|
||||
// Drop a possible entity and headers related to it
|
||||
this._requestBodyBuffers = [];
|
||||
removeMatchingHeaders(/^content-/i, this._options.headers);
|
||||
}
|
||||
|
||||
// Drop the Host header, as the redirect might lead to a different host
|
||||
var previousHostName = removeMatchingHeaders(/^host$/i, this._options.headers) ||
|
||||
url.parse(this._currentUrl).hostname;
|
||||
|
||||
// Create the redirected request
|
||||
var redirectUrl = url.resolve(this._currentUrl, location);
|
||||
debug("redirecting to", redirectUrl);
|
||||
this._isRedirect = true;
|
||||
var redirectUrlParts = url.parse(redirectUrl);
|
||||
Object.assign(this._options, redirectUrlParts);
|
||||
|
||||
// Drop the Authorization header if redirecting to another host
|
||||
if (redirectUrlParts.hostname !== previousHostName) {
|
||||
removeMatchingHeaders(/^authorization$/i, this._options.headers);
|
||||
}
|
||||
|
||||
// Evaluate the beforeRedirect callback
|
||||
if (typeof this._options.beforeRedirect === "function") {
|
||||
try {
|
||||
this._options.beforeRedirect.call(null, this._options);
|
||||
}
|
||||
catch (err) {
|
||||
this.emit("error", err);
|
||||
return;
|
||||
}
|
||||
this._sanitizeOptions(this._options);
|
||||
}
|
||||
|
||||
// Perform the redirected request
|
||||
try {
|
||||
this._performRequest();
|
||||
}
|
||||
catch (cause) {
|
||||
var error = new RedirectionError("Redirected request failed: " + cause.message);
|
||||
error.cause = cause;
|
||||
this.emit("error", error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The response is not a redirect; return it as-is
|
||||
if (!location || this._options.followRedirects === false ||
|
||||
statusCode < 300 || statusCode >= 400) {
|
||||
response.responseUrl = this._currentUrl;
|
||||
response.redirects = this._redirects;
|
||||
this.emit("response", response);
|
||||
|
||||
// Clean up
|
||||
this._requestBodyBuffers = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// The response is a redirect, so abort the current request
|
||||
abortRequest(this._currentRequest);
|
||||
// Discard the remainder of the response to avoid waiting for data
|
||||
response.destroy();
|
||||
|
||||
// RFC7231§6.4: A client SHOULD detect and intervene
|
||||
// in cyclical redirections (i.e., "infinite" redirection loops).
|
||||
if (++this._redirectCount > this._options.maxRedirects) {
|
||||
this.emit("error", new TooManyRedirectsError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the request headers if applicable
|
||||
var requestHeaders;
|
||||
var beforeRedirect = this._options.beforeRedirect;
|
||||
if (beforeRedirect) {
|
||||
requestHeaders = Object.assign({
|
||||
// The Host header was set by nativeProtocol.request
|
||||
Host: response.req.getHeader("host"),
|
||||
}, this._options.headers);
|
||||
}
|
||||
|
||||
// RFC7231§6.4: Automatic redirection needs to done with
|
||||
// care for methods not known to be safe, […]
|
||||
// RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
|
||||
// the request method from POST to GET for the subsequent request.
|
||||
var method = this._options.method;
|
||||
if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
|
||||
// RFC7231§6.4.4: The 303 (See Other) status code indicates that
|
||||
// the server is redirecting the user agent to a different resource […]
|
||||
// A user agent can perform a retrieval request targeting that URI
|
||||
// (a GET or HEAD request if using HTTP) […]
|
||||
(statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
|
||||
this._options.method = "GET";
|
||||
// Drop a possible entity and headers related to it
|
||||
this._requestBodyBuffers = [];
|
||||
removeMatchingHeaders(/^content-/i, this._options.headers);
|
||||
}
|
||||
|
||||
// Drop the Host header, as the redirect might lead to a different host
|
||||
var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
|
||||
|
||||
// If the redirect is relative, carry over the host of the last request
|
||||
var currentUrlParts = url.parse(this._currentUrl);
|
||||
var currentHost = currentHostHeader || currentUrlParts.host;
|
||||
var currentUrl = /^\w+:/.test(location) ? this._currentUrl :
|
||||
url.format(Object.assign(currentUrlParts, { host: currentHost }));
|
||||
|
||||
// Determine the URL of the redirection
|
||||
var redirectUrl;
|
||||
try {
|
||||
redirectUrl = url.resolve(currentUrl, location);
|
||||
}
|
||||
catch (cause) {
|
||||
this.emit("error", new RedirectionError(cause));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the redirected request
|
||||
debug("redirecting to", redirectUrl);
|
||||
this._isRedirect = true;
|
||||
var redirectUrlParts = url.parse(redirectUrl);
|
||||
Object.assign(this._options, redirectUrlParts);
|
||||
|
||||
// Drop confidential headers when redirecting to a less secure protocol
|
||||
// or to a different domain that is not a superdomain
|
||||
if (redirectUrlParts.protocol !== currentUrlParts.protocol &&
|
||||
redirectUrlParts.protocol !== "https:" ||
|
||||
redirectUrlParts.host !== currentHost &&
|
||||
!isSubdomain(redirectUrlParts.host, currentHost)) {
|
||||
removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers);
|
||||
}
|
||||
|
||||
// Evaluate the beforeRedirect callback
|
||||
if (typeof beforeRedirect === "function") {
|
||||
var responseDetails = {
|
||||
headers: response.headers,
|
||||
statusCode: statusCode,
|
||||
};
|
||||
var requestDetails = {
|
||||
url: currentUrl,
|
||||
method: method,
|
||||
headers: requestHeaders,
|
||||
};
|
||||
try {
|
||||
beforeRedirect(this._options, responseDetails, requestDetails);
|
||||
}
|
||||
catch (err) {
|
||||
this.emit("error", err);
|
||||
return;
|
||||
}
|
||||
this._sanitizeOptions(this._options);
|
||||
}
|
||||
|
||||
// Perform the redirected request
|
||||
try {
|
||||
this._performRequest();
|
||||
}
|
||||
catch (cause) {
|
||||
this.emit("error", new RedirectionError(cause));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -399,7 +474,7 @@ function wrap(protocols) {
|
||||
var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
|
||||
|
||||
// Executes a request, following redirects
|
||||
wrappedProtocol.request = function (input, options, callback) {
|
||||
function request(input, options, callback) {
|
||||
// Parse parameters
|
||||
if (typeof input === "string") {
|
||||
var urlStr = input;
|
||||
@@ -434,14 +509,20 @@ function wrap(protocols) {
|
||||
assert.equal(options.protocol, protocol, "protocol mismatch");
|
||||
debug("options", options);
|
||||
return new RedirectableRequest(options, callback);
|
||||
};
|
||||
}
|
||||
|
||||
// Executes a GET request, following redirects
|
||||
wrappedProtocol.get = function (input, options, callback) {
|
||||
var request = wrappedProtocol.request(input, options, callback);
|
||||
request.end();
|
||||
return request;
|
||||
};
|
||||
function get(input, options, callback) {
|
||||
var wrappedRequest = wrappedProtocol.request(input, options, callback);
|
||||
wrappedRequest.end();
|
||||
return wrappedRequest;
|
||||
}
|
||||
|
||||
// Expose the properties on the wrapped protocol
|
||||
Object.defineProperties(wrappedProtocol, {
|
||||
request: { value: request, configurable: true, enumerable: true, writable: true },
|
||||
get: { value: get, configurable: true, enumerable: true, writable: true },
|
||||
});
|
||||
});
|
||||
return exports;
|
||||
}
|
||||
@@ -477,13 +558,20 @@ function removeMatchingHeaders(regex, headers) {
|
||||
delete headers[header];
|
||||
}
|
||||
}
|
||||
return lastValue;
|
||||
return (lastValue === null || typeof lastValue === "undefined") ?
|
||||
undefined : String(lastValue).trim();
|
||||
}
|
||||
|
||||
function createErrorType(code, defaultMessage) {
|
||||
function CustomError(message) {
|
||||
function CustomError(cause) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.message = message || defaultMessage;
|
||||
if (!cause) {
|
||||
this.message = defaultMessage;
|
||||
}
|
||||
else {
|
||||
this.message = defaultMessage + ": " + cause.message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
CustomError.prototype = new Error();
|
||||
CustomError.prototype.constructor = CustomError;
|
||||
@@ -492,6 +580,19 @@ function createErrorType(code, defaultMessage) {
|
||||
return CustomError;
|
||||
}
|
||||
|
||||
function abortRequest(request) {
|
||||
for (var event of events) {
|
||||
request.removeListener(event, eventHandlers[event]);
|
||||
}
|
||||
request.on("error", noop);
|
||||
request.abort();
|
||||
}
|
||||
|
||||
function isSubdomain(subdomain, domain) {
|
||||
const dot = subdomain.length - domain.length - 1;
|
||||
return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);
|
||||
}
|
||||
|
||||
// Exports
|
||||
module.exports = wrap({ http: http, https: https });
|
||||
module.exports.wrap = wrap;
|
||||
|
||||
+18
-12
@@ -1,32 +1,33 @@
|
||||
{
|
||||
"_args": [
|
||||
[
|
||||
"follow-redirects@1.12.1",
|
||||
"/mnt/Foxconn/Digitalent/Deverloper/liff-push_2series"
|
||||
"follow-redirects@1.15.1",
|
||||
"/home/node/nuxt"
|
||||
]
|
||||
],
|
||||
"_from": "follow-redirects@1.12.1",
|
||||
"_id": "follow-redirects@1.12.1",
|
||||
"_from": "follow-redirects@1.15.1",
|
||||
"_id": "follow-redirects@1.15.1",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg==",
|
||||
"_integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"_location": "/follow-redirects",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "version",
|
||||
"registry": true,
|
||||
"raw": "follow-redirects@1.12.1",
|
||||
"raw": "follow-redirects@1.15.1",
|
||||
"name": "follow-redirects",
|
||||
"escapedName": "follow-redirects",
|
||||
"rawSpec": "1.12.1",
|
||||
"rawSpec": "1.15.1",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "1.12.1"
|
||||
"fetchSpec": "1.15.1"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/axios",
|
||||
"/http-proxy"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.1.tgz",
|
||||
"_spec": "1.12.1",
|
||||
"_where": "/mnt/Foxconn/Digitalent/Deverloper/liff-push_2series",
|
||||
"_resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"_spec": "1.15.1",
|
||||
"_where": "/home/node/nuxt",
|
||||
"author": {
|
||||
"name": "Ruben Verborgh",
|
||||
"email": "ruben@verborgh.org",
|
||||
@@ -80,6 +81,11 @@
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"name": "follow-redirects",
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/follow-redirects/follow-redirects.git"
|
||||
@@ -89,5 +95,5 @@
|
||||
"mocha": "nyc mocha",
|
||||
"test": "npm run lint && npm run mocha"
|
||||
},
|
||||
"version": "1.12.1"
|
||||
"version": "1.15.1"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user