JavaScript bindings for Mojo message validation

For each interface Foo, the Mojo JavaScript bindings now generate
a pair of functions: |validateFooRequest(validator)| and 
|validateFooResponse(messageValidator)|.

For each Mojo struct type, an additional |validate(validator)|
method is generated.

All of the validate methods return a validationError;
validationError.NONE if the message is valid or not recognized.

The JS validator class has been extended to validate structs, arrays,
and handles.

The generated methods are unit tested, in the same way as their C++
counterparts, by reading test messages and expected results from 
/mojo/public/interfaces/bindings/tests/data/validation/.

Currently the two pointer overflow test cases are skipped because
they depend on reading 64 uints, which the test message file parser
can't handle at the moment. JS doesn't support 64 bit integers. 53
bits is the limit for integer Numbers.

The message router still only checks the validity of incoming message
headers. I'd like to integrate the router with the new validation code
in a separate commit.


BUG=386808

Review URL: https://codereview.chromium.org/468713002

Cr-Commit-Position: refs/heads/master@{#290104}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@290104 0039d316-1c4b-4281-b951-d872f2087c98
parent c90f0d8d
......@@ -31,6 +31,8 @@ define("mojo/public/js/bindings/codec", [
var kStructHeaderNumBytesOffset = 0;
var kStructHeaderNumFieldsOffset = 4;
var kEncodedInvalidHandleValue = 0xFFFFFFFF;
// Decoder ------------------------------------------------------------------
function Decoder(buffer, handles, base) {
......@@ -135,7 +137,7 @@ define("mojo/public/js/bindings/codec", [
var numberOfBytes = this.readUint32();
var numberOfElements = this.readUint32();
var val = new Array(numberOfElements);
if (cls.cls === PackedBool) {
if (cls === PackedBool) {
var byte;
for (var i = 0; i < numberOfElements; ++i) {
if (i % 8 === 0)
......@@ -293,7 +295,7 @@ define("mojo/public/js/bindings/codec", [
this.writeUint32(encodedSize);
this.writeUint32(numberOfElements);
if (cls.cls === PackedBool) {
if (cls === PackedBool) {
var byte = 0;
for (i = 0; i < numberOfElements; ++i) {
if (val[i])
......@@ -328,7 +330,7 @@ define("mojo/public/js/bindings/codec", [
return;
}
var numberOfElements = val.length;
var encodedSize = kArrayHeaderSize + ((cls.cls === PackedBool) ?
var encodedSize = kArrayHeaderSize + ((cls === PackedBool) ?
Math.ceil(numberOfElements / 8) : cls.encodedSize * numberOfElements);
var encoder = this.createAndEncodeEncoder(encodedSize);
encoder.encodeArray(cls, val, numberOfElements, encodedSize);
......@@ -366,6 +368,10 @@ define("mojo/public/js/bindings/codec", [
return this.buffer.getUint32(kStructHeaderNumFieldsOffset);
};
Message.prototype.getName = function() {
return this.buffer.getUint32(kMessageNameOffset);
};
Message.prototype.getFlags = function() {
return this.buffer.getUint32(kMessageFlagsOffset);
};
......@@ -673,6 +679,7 @@ define("mojo/public/js/bindings/codec", [
exports.MessageReader = MessageReader;
exports.kArrayHeaderSize = kArrayHeaderSize;
exports.kStructHeaderSize = kStructHeaderSize;
exports.kEncodedInvalidHandleValue = kEncodedInvalidHandleValue;
exports.kMessageHeaderSize = kMessageHeaderSize;
exports.kMessageWithRequestIDHeaderSize = kMessageWithRequestIDHeaderSize;
exports.kMessageExpectsResponse = kMessageExpectsResponse;
......
......@@ -62,7 +62,7 @@ define("mojo/public/js/bindings/router", [
Router.prototype.handleIncomingMessage_ = function(message) {
var v = new validator.Validator(message);
if (v.validateMessage() !== validator.validationError.NONE)
if (v.validateMessageHeader() !== validator.validationError.NONE)
this.close();
if (message.expectsResponse()) {
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
define([
"console",
"file",
"gin/test/expect",
"mojo/public/interfaces/bindings/tests/validation_test_interfaces.mojom",
......@@ -10,7 +11,8 @@ define([
"mojo/public/js/bindings/codec",
"mojo/public/js/bindings/tests/validation_test_input_parser",
"mojo/public/js/bindings/validator",
], function(file, expect, testInterface, buffer, codec, parser, validator) {
], function(
console, file, expect, testInterface, buffer, codec, parser, validator) {
function checkTestMessageParser() {
function TestMessageParserFailure(message, input) {
......@@ -162,7 +164,7 @@ define([
return null;
}
function getMessageTestFiles() {
function getMessageTestFiles(key) {
var sourceRoot = file.getSourceRootDirectory();
expect(sourceRoot).not.toBeNull();
......@@ -172,17 +174,14 @@ define([
expect(testFiles).not.toBeNull();
expect(testFiles.length).toBeGreaterThan(0);
// The ".data" pathnames with the extension removed.
var testPathNames = testFiles.filter(function(s) {
return s.substr(-5) == ".data";
}).map(function(s) {
return testDir + s.slice(0, -5);
});
// For now, just checking the message header tests.
return testPathNames.filter(function(s) {
return s.indexOf("_msghdr_") != -1;
});
// The matching ".data" pathnames with the extension removed.
return testFiles.filter(function(s) {
return s.substr(-5) == ".data";
}).map(function(s) {
return testDir + s.slice(0, -5);
}).filter(function(s) {
return s.indexOf(key) != -1;
});
}
function readTestMessage(filename) {
......@@ -197,21 +196,57 @@ define([
return contents.trim();
}
function testValidateMessageHeader() {
var testFiles = getMessageTestFiles();
function testMessageValidation(key, filters) {
var testFiles = getMessageTestFiles(key);
expect(testFiles.length).toBeGreaterThan(0);
var noError = validator.validationError.NONE;
for (var i = 0; i < testFiles.length; i++) {
// TODO(hansmuller): Temporarily skipping array pointer overflow tests.
if (testFiles[i].indexOf("overflow") != -1) {
console.log("[Skipping " + testFiles[i] + "]");
continue;
}
// TODO(hansmuller): Temporarily skipping array unexpected null tests.
if (testFiles[i].indexOf("unexpected_null") != -1 ||
testFiles[i].indexOf("unexpected_invalid") != -1) {
console.log("[Skipping " + testFiles[i] + "]");
continue;
}
var testMessage = readTestMessage(testFiles[i]);
// TODO(hansmuller): add the message handles.
var message = new codec.Message(testMessage.buffer);
var actualResult = new validator.Validator(message).validateMessage();
var handles = new Array(testMessage.handleCount);
var message = new codec.Message(testMessage.buffer, handles);
var messageValidator = new validator.Validator(message);
var err = messageValidator.validateMessageHeader();
for (var j = 0; err === noError && j < filters.length; ++j)
err = filters[j](messageValidator);
var actualResult = (err === noError) ? "PASS" : err;
var expectedResult = readTestExpected(testFiles[i]);
if (actualResult != expectedResult)
console.log("[Test message validation failed: " + testFiles[i] + " ]");
expect(actualResult).toEqual(expectedResult);
}
}
testValidateMessageHeader();
function testConformanceMessageValidation() {
testMessageValidation("conformance_", [
testInterface.validateConformanceTestInterfaceRequest,
]);
}
function testIntegrationMessageValidation() {
testMessageValidation("integration_", [
testInterface.validateIntegrationTestInterface1Request,
testInterface.validateIntegrationTestInterface2Response
]);
}
testConformanceMessageValidation();
testIntegrationMessageValidation();
expect(checkTestMessageParser()).toBeNull();
this.result = "PASS";
});
......@@ -4,7 +4,7 @@
define("mojo/public/js/bindings/validator", [
"mojo/public/js/bindings/codec",
], function(codec) {
], function(codec) {
var validationError = {
NONE: 'VALIDATION_ERROR_NONE',
......@@ -20,15 +20,22 @@ define("mojo/public/js/bindings/validator", [
'VALIDATION_ERROR_MESSAGE_HEADER_MISSING_REQUEST_ID'
};
var NULL_MOJO_POINTER = "NULL_MOJO_POINTER";
function Validator(message) {
this.message = message;
this.offset = 0;
this.handleIndex = 0;
}
Object.defineProperty(Validator.prototype, "offsetLimit", {
get: function() { return this.message.buffer.byteLength; }
});
Object.defineProperty(Validator.prototype, "handleIndexLimit", {
get: function() { return this.message.handles.length; }
});
// True if we can safely allocate a block of bytes from start to
// to start + numBytes.
Validator.prototype.isValidRange = function(start, numBytes) {
......@@ -54,6 +61,25 @@ define("mojo/public/js/bindings/validator", [
return false;
}
Validator.prototype.claimHandle = function(index) {
if (index === codec.kEncodedInvalidHandleValue)
return true;
if (index < this.handleIndex || index >= this.handleIndexLimit)
return false;
// This is safe because handle indices are uint32.
this.handleIndex = index + 1;
return true;
}
Validator.prototype.validateHandle = function(offset) {
var index = this.message.buffer.getUint32(offset);
if (!this.claimHandle(index))
return validationError.ILLEGAL_HANDLE;
return validationError.NONE;
}
Validator.prototype.validateStructHeader =
function(offset, minNumBytes, minNumFields) {
if (!codec.isAligned(offset))
......@@ -75,6 +101,10 @@ define("mojo/public/js/bindings/validator", [
}
Validator.prototype.validateMessageHeader = function() {
var err = this.validateStructHeader(0, codec.kMessageHeaderSize, 2);
if (err != validationError.NONE)
return err;
var numBytes = this.message.getHeaderNumBytes();
var numFields = this.message.getHeaderNumFields();
......@@ -99,12 +129,126 @@ define("mojo/public/js/bindings/validator", [
return validationError.NONE;
}
Validator.prototype.validateMessage = function() {
var err = this.validateStructHeader(0, codec.kStructHeaderSize, 2);
if (err != validationError.NONE)
return err;
// Returns the message.buffer relative offset this pointer "points to",
// NULL_MOJO_POINTER if the pointer represents a null, or JS null if the
// pointer's value is not valid.
Validator.prototype.decodePointer = function(offset) {
var pointerValue = this.message.buffer.getUint64(offset);
if (pointerValue === 0)
return NULL_MOJO_POINTER;
var bufferOffset = offset + pointerValue;
return Number.isSafeInteger(bufferOffset) ? bufferOffset : null;
}
Validator.prototype.validateArrayPointer =
function(offset, elementSize, expectedElementCount, elementType) {
var arrayOffset = this.decodePointer(offset);
if (arrayOffset === null)
return validationError.ILLEGAL_POINTER;
if (arrayOffset === NULL_MOJO_POINTER)
return validationError.NONE;
return this.validateArray(
arrayOffset, elementSize, expectedElementCount, elementType);
}
return this.validateMessageHeader();
Validator.prototype.validateStructPointer = function(offset, structClass) {
var structOffset = this.decodePointer(offset);
if (structOffset === null)
return validationError.ILLEGAL_POINTER;
if (structOffset === NULL_MOJO_POINTER)
return validationError.NONE;
return structClass.validate(this, structOffset);
}
Validator.prototype.validateStringPointer = function(offset) {
return this.validateArrayPointer(
offset, codec.Uint8.encodedSize, 0, codec.Uint8);
}
// Similar to Array_Data<T>::Validate()
// mojo/public/cpp/bindings/lib/array_internal.h
Validator.prototype.validateArray =
function (offset, elementSize, expectedElementCount, elementType) {
if (!codec.isAligned(offset))
return validationError.MISALIGNED_OBJECT;
if (!this.isValidRange(offset, codec.kArrayHeaderSize))
return validationError.ILLEGAL_MEMORY_RANGE;
var numBytes = this.message.buffer.getUint32(offset);
var numElements = this.message.buffer.getUint32(offset + 4);
// Note: this computation is "safe" because elementSize <= 8 and
// numElements is a uint32.
var elementsTotalSize = (elementType === codec.PackedBool) ?
Math.ceil(numElements / 8) : (elementSize * numElements);
if (numBytes < codec.kArrayHeaderSize + elementsTotalSize)
return validationError.UNEXPECTED_ARRAY_HEADER;
if (expectedElementCount != 0 && numElements != expectedElementCount)
return validationError.UNEXPECTED_ARRAY_HEADER;
if (!this.claimRange(offset, numBytes))
return validationError.ILLEGAL_MEMORY_RANGE;
// Validate the array's elements if they are pointers or handles.
var elementsOffset = offset + codec.kArrayHeaderSize;
if (elementType === codec.Handle)
return this.validateHandleElements(elementsOffset, numElements);
if (elementType instanceof codec.PointerTo)
return this.validateStructElements(
elementsOffset, numElements, elementType.cls);
if (elementType instanceof codec.String)
return this.validateArrayElements(
elementsOffset, numElements, codec.Uint8);
if (elementType instanceof codec.ArrayOf)
return this.validateArrayElements(
elementsOffset, numElements, elementType.cls);
return validationError.NONE;
}
// Note: the |offset + i * elementSize| computation in the validateFooElements
// methods below is "safe" because elementSize <= 8, offset and
// numElements are uint32, and 0 <= i < numElements.
Validator.prototype.validateHandleElements = function(offset, numElements) {
var elementSize = codec.Handle.encodedSize;
for (var i = 0; i < numElements; i++) {
var index = this.message.buffer.getUint32(offset + i * elementSize);
if (!this.claimHandle(index))
return validationError.ILLEGAL_HANDLE;
}
return validationError.NONE;
}
// The elementClass parameter is the element type of the element arrays.
Validator.prototype.validateArrayElements =
function(offset, numElements, elementClass) {
var elementSize = codec.PointerTo.prototype.encodedSize;
for (var i = 0; i < numElements; i++) {
var elementOffset = offset + i * elementSize;
var err = this.validateArrayPointer(
elementOffset, elementClass.encodedSize, 0, elementClass);
if (err != validationError.NONE)
return err;
}
return validationError.NONE;
}
Validator.prototype.validateStructElements =
function(offset, numElements, structClass) {
var elementSize = codec.PointerTo.prototype.encodedSize;
for (var i = 0; i < numElements; i++) {
var elementOffset = offset + i * elementSize;
var err = this.validateStructPointer(elementOffset, structClass);
if (err != validationError.NONE)
return err;
}
return validationError.NONE;
}
var exports = {};
......
......@@ -108,6 +108,55 @@ params.{{parameter.name}}{% if not loop.last %}, {% endif -%}
}
};
{#--- Validation #}
function validate{{interface.name}}Request(messageValidator) {
{%- if not(interface.methods) %}
return validator.validationError.NONE;
{%- else %}
var message = messageValidator.message;
var paramsClass = null;
switch (message.getName()) {
{%- for method in interface.methods %}
case k{{interface.name}}_{{method.name}}_Name:
{%- if method.response_parameters == None %}
if (!message.expectsResponse() && !message.isResponse())
paramsClass = {{interface.name}}_{{method.name}}_Params;
{%- else %}
if (message.expectsResponse())
paramsClass = {{interface.name}}_{{method.name}}_Params;
{%- endif %}
break;
{%- endfor %}
}
if (paramsClass === null)
return validator.validationError.NONE;
return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes());
{%- endif %}
}
function validate{{interface.name}}Response(messageValidator) {
{%- if not(interface|has_callbacks) %}
return validator.validationError.NONE;
{%- else %}
var message = messageValidator.message;
var paramsClass = null;
switch (message.getName()) {
{%- for method in interface.methods %}
{%- if method.response_parameters != None %}
case k{{interface.name}}_{{method.name}}_Name:
if (message.isResponse())
paramsClass = {{interface.name}}_{{method.name}}_ResponseParams;
break;
{%- endif %}
{%- endfor %}
}
if (paramsClass === null)
return validator.validationError.NONE;
return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes());
{%- endif %}
}
{#--- Enums #}
{% from "enum_definition.tmpl" import enum_def -%}
{% for enum in interface.enums %}
......
......@@ -4,10 +4,11 @@
define("{{module.path}}", [
"mojo/public/js/bindings/codec",
"mojo/public/js/bindings/validator",
{%- for import in imports %}
"{{import.module.path}}",
{%- endfor %}
], function(codec
], function(codec, validator
{%- for import in imports -%}
, {{import.unique_name}}
{%- endfor -%}
......@@ -47,6 +48,8 @@ define("{{module.path}}", [
{%- for interface in interfaces %}
exports.{{interface.name}}Proxy = {{interface.name}}Proxy;
exports.{{interface.name}}Stub = {{interface.name}}Stub;
exports.validate{{interface.name}}Request = validate{{interface.name}}Request;
exports.validate{{interface.name}}Response = validate{{interface.name}}Response;
{%- endfor %}
return exports;
});
......@@ -21,6 +21,36 @@
{%- endfor %}
};
{#--- Validation #}
{{struct.name}}.validate = function(messageValidator, offset) {
var err;
err = messageValidator.validateStructHeader(offset, {{struct.name}}.encodedSize, {{struct.packed.packed_fields|length}});
if (err !== validator.validationError.NONE)
return err;
{%- for packed_field in struct.packed.packed_fields %}
{%- set field_name = packed_field.field.name %}
{%- if packed_field.field|is_string_pointer_field %}
// validate {{struct.name}}.{{field_name}}
err = messageValidator.validateStringPointer({{packed_field|field_offset}})
{%- elif packed_field.field|is_array_pointer_field %}
// validate {{struct.name}}.{{field_name}}
err = messageValidator.validateArrayPointer({{packed_field|validate_array_params}});
{%- elif packed_field.field|is_struct_pointer_field %}
// validate {{struct.name}}.{{field_name}}
err = messageValidator.validateStructPointer({{packed_field|validate_struct_params}});
{%- elif packed_field.field|is_handle_field %}
// validate {{struct.name}}.{{field_name}}
err = messageValidator.validateHandle({{packed_field|field_offset}})
{%- endif %}
if (err !== validator.validationError.NONE)
return err;
{%- endfor %}
return validator.validationError.NONE;
};
{#--- Encoding and decoding #}
{{struct.name}}.encodedSize = codec.kStructHeaderSize + {{struct.packed|payload_size}};
......
......@@ -228,12 +228,6 @@ def TranslateConstants(token, kind):
def ExpressionToText(value, kind=None):
return TranslateConstants(value, kind)
def HasCallbacks(interface):
for method in interface.methods:
if method.response_parameters != None:
return True
return False
def ShouldInlineStruct(struct):
# TODO(darin): Base this on the size of the wrapper class.
if len(struct.fields) > 4:
......@@ -279,7 +273,7 @@ class Generator(generator.Generator):
"get_array_validate_params": GetArrayValidateParams,
"get_name_for_kind": GetNameForKind,
"get_pad": pack.GetPad,
"has_callbacks": HasCallbacks,
"has_callbacks": mojom.HasCallbacks,
"should_inline": ShouldInlineStruct,
"is_any_array_kind": mojom.IsAnyArrayKind,
"is_enum_kind": mojom.IsEnumKind,
......
......@@ -46,7 +46,7 @@ def JavaScriptDefaultValue(field):
return _kind_to_javascript_default_value[field.kind]
if mojom.IsStructKind(field.kind):
return "null"
if mojom.IsArrayKind(field.kind):
if mojom.IsAnyArrayKind(field.kind):
return "null"
if mojom.IsInterfaceKind(field.kind) or \
mojom.IsInterfaceRequestKind(field.kind):
......@@ -97,9 +97,9 @@ def CodecType(kind):
return _kind_to_codec_type[kind]
if mojom.IsStructKind(kind):
return "new codec.PointerTo(%s)" % CodecType(kind.name)
if mojom.IsArrayKind(kind) and mojom.IsBoolKind(kind.kind):
return "new codec.ArrayOf(new codec.ArrayOf(codec.PackedBool))"
if mojom.IsArrayKind(kind):
if mojom.IsAnyArrayKind(kind) and mojom.IsBoolKind(kind.kind):
return "new codec.ArrayOf(codec.PackedBool)"
if mojom.IsAnyArrayKind(kind):
return "new codec.ArrayOf(%s)" % CodecType(kind.kind)
if mojom.IsInterfaceKind(kind) or mojom.IsInterfaceRequestKind(kind):
return CodecType(mojom.MSGPIPE)
......@@ -113,9 +113,9 @@ def JavaScriptDecodeSnippet(kind):
return "decodeStruct(%s)" % CodecType(kind)
if mojom.IsStructKind(kind):
return "decodeStructPointer(%s)" % CodecType(kind.name)
if mojom.IsArrayKind(kind) and mojom.IsBoolKind(kind.kind):
return "decodeArrayPointer(new codec.ArrayOf(codec.PackedBool))"
if mojom.IsArrayKind(kind):
if mojom.IsAnyArrayKind(kind) and mojom.IsBoolKind(kind.kind):
return "decodeArrayPointer(codec.PackedBool)"
if mojom.IsAnyArrayKind(kind):
return "decodeArrayPointer(%s)" % CodecType(kind.kind)
if mojom.IsInterfaceKind(kind) or mojom.IsInterfaceRequestKind(kind):
return JavaScriptDecodeSnippet(mojom.MSGPIPE)
......@@ -128,8 +128,8 @@ def JavaScriptEncodeSnippet(kind):
return "encodeStruct(%s, " % CodecType(kind)
if mojom.IsStructKind(kind):
return "encodeStructPointer(%s, " % CodecType(kind.name)
if mojom.IsArrayKind(kind) and mojom.IsBoolKind(kind.kind):
return "encodeArrayPointer(new codec.ArrayOf(codec.PackedBool), ";
if mojom.IsAnyArrayKind(kind) and mojom.IsBoolKind(kind.kind):
return "encodeArrayPointer(codec.PackedBool, ";
if mojom.IsAnyArrayKind(kind):
return "encodeArrayPointer(%s, " % CodecType(kind.kind)
if mojom.IsInterfaceKind(kind) or mojom.IsInterfaceRequestKind(kind):
......@@ -138,6 +138,22 @@ def JavaScriptEncodeSnippet(kind):
return JavaScriptEncodeSnippet(mojom.INT32)
def JavaScriptFieldOffset(packed_field):
return "offset + codec.kStructHeaderSize + %s" % packed_field.offset
def JavaScriptValidateArrayParams(pf):
elementKind = pf.field.kind.kind
elementSize = pack.PackedField.GetSizeForKind(elementKind)
elementCount = generator.ExpectedArraySize(pf.field.kind)
elementType = "codec.PackedBool" if mojom.IsBoolKind(elementKind) \
else CodecType(elementKind)
return "%s, %s, %s, %s" % \
(JavaScriptFieldOffset(pf), elementSize, elementCount, elementType)
def JavaScriptValidateStructParams(pf):
return "%s, %s" % (JavaScriptFieldOffset(pf), pf.field.kind.name)
def TranslateConstants(token):
if isinstance(token, (mojom.EnumValue, mojom.NamedValue)):
# Both variable and enum constants are constructed like:
......@@ -163,6 +179,18 @@ def JavascriptType(kind):
return kind.imported_from["unique_name"] + "." + kind.name
return kind.name
def IsArrayPointerField(field):
return mojom.IsAnyArrayKind(field.kind)
def IsStringPointerField(field):
return mojom.IsStringKind(field.kind)
def IsStructPointerField(field):
return mojom.IsStructKind(field.kind)
def IsHandleField(field):
return mojom.IsAnyHandleKind(field.kind)
class Generator(generator.Generator):
......@@ -172,8 +200,16 @@ class Generator(generator.Generator):
"decode_snippet": JavaScriptDecodeSnippet,
"encode_snippet": JavaScriptEncodeSnippet,
"expression_to_text": ExpressionToText,
"field_offset": JavaScriptFieldOffset,
"has_callbacks": mojom.HasCallbacks,
"is_array_pointer_field": IsArrayPointerField,
"is_struct_pointer_field": IsStructPointerField,
"is_string_pointer_field": IsStringPointerField,
"is_handle_field": IsHandleField,
"js_type": JavascriptType,
"stylize_method": generator.StudlyCapsToCamel,
"validate_array_params": JavaScriptValidateArrayParams,
"validate_struct_params": JavaScriptValidateStructParams,
}
@UseJinja("js_templates/module.js.tmpl", filters=js_filters)
......
......@@ -400,3 +400,11 @@ def IsAnyHandleKind(kind):
def IsMoveOnlyKind(kind):
return IsObjectKind(kind) or IsAnyHandleKind(kind)
def HasCallbacks(interface):
for method in interface.methods:
if method.response_parameters != None:
return True
return False
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment