Update to support OEMCrypto v16 with ODK
This commit is contained in:
@@ -36,7 +36,7 @@ cc_library(
|
||||
deps = [
|
||||
"//base",
|
||||
"//common:certificate_type",
|
||||
"//protos/public:certificate_provisioning_proto",
|
||||
"//protos/public:certificate_provisioning_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -56,8 +56,10 @@ cc_library(
|
||||
"//provisioning_sdk/internal:provisioning30_session_impl",
|
||||
"//provisioning_sdk/internal:provisioning_engine_impl",
|
||||
"//provisioning_sdk/internal:provisioning_session_impl",
|
||||
"//protos/public:certificate_provisioning_proto",
|
||||
"//protos/public:certificate_provisioning_cc_proto",
|
||||
],
|
||||
# Make sure libprovisioning_sdk links in symbols defined in this target.
|
||||
alwayslink = 1,
|
||||
)
|
||||
|
||||
cc_test(
|
||||
@@ -79,7 +81,7 @@ cc_library(
|
||||
":provisioning_status",
|
||||
"//base",
|
||||
"//provisioning_sdk/internal:provisioning_session_impl",
|
||||
"//protos/public:drm_certificate_proto",
|
||||
"//protos/public:drm_certificate_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ ProvisioningEngine::ProvisioningEngine() {}
|
||||
ProvisioningEngine::~ProvisioningEngine() {}
|
||||
|
||||
ProvisioningStatus ProvisioningEngine::Initialize(
|
||||
CertificateType certificate_type, const std::string& service_drm_certificate,
|
||||
CertificateType certificate_type,
|
||||
const std::string& service_drm_certificate,
|
||||
const std::string& service_private_key,
|
||||
const std::string& service_private_key_passphrase,
|
||||
const std::string& provisioning_drm_certificate,
|
||||
@@ -64,20 +65,22 @@ ProvisioningStatus ProvisioningEngine::Initialize(
|
||||
}
|
||||
|
||||
void ProvisioningEngine::RegisterProtocol(
|
||||
SignedProvisioningMessage::ProtocolVersion protocol,
|
||||
int protocol,
|
||||
SessionFactory session_factory) {
|
||||
protocol_registry_[protocol] = std::move(session_factory);
|
||||
}
|
||||
|
||||
ProvisioningStatus ProvisioningEngine::SetCertificateStatusList(
|
||||
const std::string& certificate_status_list, uint32_t expiration_period_seconds) {
|
||||
const std::string& certificate_status_list,
|
||||
uint32_t expiration_period_seconds) {
|
||||
if (!impl_) return PROVISIONING_ENGINE_UNINITIALIZED;
|
||||
return impl_->SetCertificateStatusList(certificate_status_list,
|
||||
expiration_period_seconds);
|
||||
}
|
||||
|
||||
ProvisioningStatus ProvisioningEngine::GenerateDrmIntermediateCertificate(
|
||||
uint32_t system_id, const std::string& public_key, std::string* certificate) const {
|
||||
uint32_t system_id, const std::string& public_key,
|
||||
std::string* certificate) const {
|
||||
if (!impl_) return PROVISIONING_ENGINE_UNINITIALIZED;
|
||||
if (!certificate) {
|
||||
LOG(WARNING) << "|certificate| should not be a nullptr.";
|
||||
@@ -97,7 +100,6 @@ ProvisioningStatus ProvisioningEngine::AddDrmIntermediateCertificate(
|
||||
}
|
||||
|
||||
ProvisioningStatus ProvisioningEngine::NewProvisioningSession(
|
||||
SignedProvisioningMessage::ProtocolVersion protocol,
|
||||
const std::string& device_public_key, const std::string& device_private_key,
|
||||
std::unique_ptr<ProvisioningSession>* new_session) const {
|
||||
if (!impl_) return PROVISIONING_ENGINE_UNINITIALIZED;
|
||||
@@ -106,6 +108,7 @@ ProvisioningStatus ProvisioningEngine::NewProvisioningSession(
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
const int protocol = 3; // Provisioning 3.0
|
||||
auto factory = protocol_registry_.find(protocol);
|
||||
if (factory == protocol_registry_.end()) {
|
||||
LOG(WARNING) << "Provisioning protocol not supported (" << protocol << ")";
|
||||
@@ -124,45 +127,17 @@ ProvisioningStatus ProvisioningEngine::NewProvisioningSession(
|
||||
}
|
||||
|
||||
std::unique_ptr<ProvisioningSession> ProvisioningEngine::NewProvisioningSession(
|
||||
SignedProvisioningMessage::ProtocolVersion protocol,
|
||||
const std::string& device_public_key, const std::string& device_private_key,
|
||||
ProvisioningStatus* status) const {
|
||||
std::unique_ptr<ProvisioningSession> new_session;
|
||||
*status = NewProvisioningSession(protocol, device_public_key,
|
||||
device_private_key, &new_session);
|
||||
*status = NewProvisioningSession(device_public_key,
|
||||
device_private_key, &new_session);
|
||||
return new_session;
|
||||
}
|
||||
|
||||
ProvisioningStatus ProvisioningEngine::NewKeyboxProvisioningSession(
|
||||
const std::string& keybox_device_key,
|
||||
std::unique_ptr<ProvisioningSession>* new_session) const {
|
||||
if (!impl_) return PROVISIONING_ENGINE_UNINITIALIZED;
|
||||
if (!new_session) {
|
||||
LOG(WARNING) << "|new_session| should not be a nullptr.";
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
SignedProvisioningMessage::ProtocolVersion protocol =
|
||||
SignedProvisioningMessage::ARCPP_PROVISIONING;
|
||||
auto factory = protocol_registry_.find(protocol);
|
||||
if (factory == protocol_registry_.end()) {
|
||||
LOG(WARNING) << "Provisioning protocol not supported (" << protocol << ")";
|
||||
return INVALID_PROTOCOL;
|
||||
}
|
||||
std::unique_ptr<ProvisioningSessionImpl> session_impl;
|
||||
ProvisioningStatus status = (factory->second)(*impl_, &session_impl);
|
||||
if (status != OK) return status;
|
||||
|
||||
status = session_impl->Initialize(keybox_device_key);
|
||||
if (status != OK) return status;
|
||||
|
||||
new_session->reset(new ProvisioningSession(std::move(session_impl)));
|
||||
return OK;
|
||||
}
|
||||
|
||||
ProvisioningStatus ProvisioningEngine::GenerateDeviceDrmCertificate(
|
||||
uint32_t system_id, const std::string& public_key, const std::string& serial_number,
|
||||
std::string* certificate) const {
|
||||
uint32_t system_id, const std::string& public_key,
|
||||
const std::string& serial_number, std::string* certificate) const {
|
||||
if (!impl_) return PROVISIONING_ENGINE_UNINITIALIZED;
|
||||
if (!certificate) {
|
||||
LOG(WARNING) << "|certificate| should not be a nullptr.";
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#include "common/certificate_type.h"
|
||||
#include "provisioning_sdk/public/provisioning_status.h"
|
||||
#include "protos/public/certificate_provisioning.pb.h"
|
||||
|
||||
namespace widevine {
|
||||
|
||||
@@ -65,7 +64,8 @@ class ProvisioningEngine {
|
||||
// derivation of Stable Per-Origin IDentifiers.
|
||||
// * Returns OK on success, or an appropriate error status code otherwise.
|
||||
ProvisioningStatus Initialize(
|
||||
CertificateType certificate_type, const std::string& service_drm_certificate,
|
||||
CertificateType certificate_type,
|
||||
const std::string& service_drm_certificate,
|
||||
const std::string& service_private_key,
|
||||
const std::string& service_private_key_passphrase,
|
||||
const std::string& provisioning_drm_certificate,
|
||||
@@ -78,7 +78,7 @@ class ProvisioningEngine {
|
||||
// SignedProvisioningMessage message.
|
||||
// * |session_factory| is the function which instantiates the appropriate
|
||||
// ProvisioningSessionImpl object for the specified protocol.
|
||||
void RegisterProtocol(SignedProvisioningMessage::ProtocolVersion protocol,
|
||||
void RegisterProtocol(int protocol,
|
||||
SessionFactory session_factory);
|
||||
|
||||
// Set the certificate status list for this engine.
|
||||
@@ -89,7 +89,8 @@ class ProvisioningEngine {
|
||||
// (creation_time_seconds). Zero means it will never expire.
|
||||
// * Returns OK on success, or an appropriate error status code otherwise.
|
||||
virtual ProvisioningStatus SetCertificateStatusList(
|
||||
const std::string& certificate_status_list, uint32_t expiration_period_seconds);
|
||||
const std::string& certificate_status_list,
|
||||
uint32_t expiration_period_seconds);
|
||||
|
||||
// Generate an intermediate DRM certificate.
|
||||
// * |system_id| is the Widevine system ID for the type of device.
|
||||
@@ -103,7 +104,8 @@ class ProvisioningEngine {
|
||||
// engines, including this one, by invoking
|
||||
// |AddIntermediatedrmcertificate| on all active ProvisioningEngine(s).
|
||||
ProvisioningStatus GenerateDrmIntermediateCertificate(
|
||||
uint32_t system_id, const std::string& public_key, std::string* certificate) const;
|
||||
uint32_t system_id, const std::string& public_key,
|
||||
std::string* certificate) const;
|
||||
|
||||
// Add an intermediate DRM certificate to the provisioning engine. This is
|
||||
// usually done once for each supported device type.
|
||||
@@ -134,29 +136,15 @@ class ProvisioningEngine {
|
||||
// NOTE: All ProvisioningSession objects must be deleted before the
|
||||
// ProvisioningEngine which created them.
|
||||
virtual ProvisioningStatus NewProvisioningSession(
|
||||
SignedProvisioningMessage::ProtocolVersion protocol,
|
||||
const std::string& device_public_key, const std::string& device_private_key,
|
||||
const std::string& device_public_key,
|
||||
const std::string& device_private_key,
|
||||
std::unique_ptr<ProvisioningSession>* new_session) const;
|
||||
|
||||
// This is the same as NewProvisioningSession above, but with outputs reversed
|
||||
// To get around CLIF bug https://github.com/google/clif/issues/30.
|
||||
std::unique_ptr<ProvisioningSession> NewProvisioningSession(
|
||||
SignedProvisioningMessage::ProtocolVersion protocol,
|
||||
const std::string& device_public_key, const std::string& device_private_key,
|
||||
ProvisioningStatus* status) const;
|
||||
|
||||
// Create a session to handle a keybox provisioning exchange between
|
||||
// a client device (e.g., ChromeOS) and the provisioning server.
|
||||
// It would use ARCPP_PROVISIONING protocol.
|
||||
// * |keybox_device_key| is the secret device key in the keybox.
|
||||
// * |new_session| will point, on successful return, to the newly created
|
||||
// ProvisioningSession.
|
||||
// * Returns OK if successful, or an appropriate error status code otherwise.
|
||||
// NOTE: All ProvisioningSession objects must be deleted before the
|
||||
// ProvisioningEngine which created them.
|
||||
virtual ProvisioningStatus NewKeyboxProvisioningSession(
|
||||
const std::string& keybox_device_key,
|
||||
std::unique_ptr<ProvisioningSession>* new_session) const;
|
||||
const std::string& device_public_key,
|
||||
const std::string& device_private_key, ProvisioningStatus* status) const;
|
||||
|
||||
// Generate a new device DRM certificate to be provisioned by means other than
|
||||
// the Widevine provisioning protocol.
|
||||
@@ -173,13 +161,12 @@ class ProvisioningEngine {
|
||||
// * |certificate| will contain, upon successful return the generated
|
||||
// certificate.
|
||||
// * Returns OK on success, or an appropriate error status code otherwise.
|
||||
ProvisioningStatus GenerateDeviceDrmCertificate(uint32_t system_id,
|
||||
const std::string& public_key,
|
||||
const std::string& serial_number,
|
||||
std::string* certificate) const;
|
||||
ProvisioningStatus GenerateDeviceDrmCertificate(
|
||||
uint32_t system_id, const std::string& public_key,
|
||||
const std::string& serial_number, std::string* certificate) const;
|
||||
|
||||
private:
|
||||
std::map<SignedProvisioningMessage::ProtocolVersion, SessionFactory>
|
||||
std::map<int, SessionFactory>
|
||||
protocol_registry_;
|
||||
|
||||
#ifndef SWIGPYTHON
|
||||
|
||||
@@ -26,9 +26,8 @@ ProvisioningSession::ProvisioningSession() {}
|
||||
|
||||
ProvisioningSession::~ProvisioningSession() {}
|
||||
|
||||
ProvisioningStatus ProvisioningSession::ProcessMessage(const std::string& message,
|
||||
std::string* response,
|
||||
bool* done) {
|
||||
ProvisioningStatus ProvisioningSession::ProcessMessage(
|
||||
const std::string& message, std::string* response, bool* done) {
|
||||
if (!response) {
|
||||
LOG(WARNING) << "|response| should not be a nullptr.";
|
||||
return INTERNAL_ERROR;
|
||||
|
||||
@@ -32,8 +32,7 @@ class ProvisioningSession {
|
||||
// exchange is complete.
|
||||
// Returns OK if successful, or an appropriate error status code otherwise.
|
||||
virtual ProvisioningStatus ProcessMessage(const std::string& message,
|
||||
std::string* response,
|
||||
bool* done);
|
||||
std::string* response, bool* done);
|
||||
|
||||
// * Returns a ProvisioneddeviceInfo message containing information about the
|
||||
// type of device being provisioned. May return nullptr.
|
||||
|
||||
@@ -11,76 +11,7 @@ package(default_visibility = ["//visibility:public"])
|
||||
filegroup(
|
||||
name = "binary_release_files",
|
||||
srcs = glob([
|
||||
"*.clif",
|
||||
"*.py",
|
||||
"*.i",
|
||||
]),
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "test_data_utility",
|
||||
srcs = [
|
||||
"test_data_provider.py",
|
||||
"test_data_utility.py",
|
||||
],
|
||||
data = [
|
||||
"//example:example_data",
|
||||
],
|
||||
deps = [
|
||||
"//protos/public:certificate_provisioning_py_pb2",
|
||||
],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "crypto_utility",
|
||||
srcs = ["crypto_utility.py"],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "init_engine_test",
|
||||
size = "small",
|
||||
srcs = ["init_engine_test.py"],
|
||||
deps = [
|
||||
":test_data_utility",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "set_certificate_status_list_test",
|
||||
size = "small",
|
||||
srcs = ["set_certificate_status_list_test.py"],
|
||||
deps = [
|
||||
":test_data_utility",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "drm_intermediate_certificate_test",
|
||||
size = "small",
|
||||
srcs = ["drm_intermediate_certificate_test.py"],
|
||||
deps = [
|
||||
":test_data_utility",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "engine_generate_certificate_test",
|
||||
size = "small",
|
||||
srcs = ["engine_generate_certificate_test.py"],
|
||||
deps = [
|
||||
":crypto_utility",
|
||||
":test_data_utility",
|
||||
"//protos/public:signed_drm_certificate_py_pb2",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "new_session_test",
|
||||
size = "small",
|
||||
srcs = ["new_session_test.py"],
|
||||
deps = [
|
||||
":crypto_utility",
|
||||
":test_data_utility",
|
||||
"//protos/public:certificate_provisioning_py_pb2",
|
||||
"//protos/public:signed_drm_certificate_py_pb2",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import test_data_provider
|
||||
import test_data_utility
|
||||
from provisioning_engine import ProvisioningEngine
|
||||
from provisioning_status import ProvisioningStatus
|
||||
from protos.public import certificate_provisioning_pb2
|
||||
|
||||
|
||||
class InitEngineTest(unittest.TestCase):
|
||||
@@ -22,8 +21,6 @@ class InitEngineTest(unittest.TestCase):
|
||||
self._engine = ProvisioningEngine()
|
||||
self._data_provider = test_data_provider.TestDataProvider(
|
||||
CertificateType.kCertificateTypeTesting)
|
||||
self._prov30 = (
|
||||
certificate_provisioning_pb2.SignedProvisioningMessage.PROVISIONING_30)
|
||||
|
||||
def testInitEngineSucceed(self):
|
||||
status = test_data_utility.InitProvisionEngineWithTestData(
|
||||
@@ -57,7 +54,8 @@ class InitEngineTest(unittest.TestCase):
|
||||
|
||||
def testNewProvisioningSessionWithoutInit(self):
|
||||
session, status = self._engine.NewProvisioningSession(
|
||||
self._prov30, 'DEVICE_PUBLIC_KEY', 'DEVICE_PRIVATE_KEY')
|
||||
'DEVICE_PUBLIC_KEY',
|
||||
'DEVICE_PRIVATE_KEY')
|
||||
self.assertEqual(ProvisioningStatus.PROVISIONING_ENGINE_UNINITIALIZED,
|
||||
status)
|
||||
self.assertIsNone(session)
|
||||
|
||||
@@ -28,8 +28,6 @@ class NewSessionTest(unittest.TestCase):
|
||||
self._engine, 0, verify_success=True)
|
||||
self._data_provider = test_data_provider.TestDataProvider(
|
||||
CertificateType.kCertificateTypeTesting)
|
||||
self._prov30 = (
|
||||
certificate_provisioning_pb2.SignedProvisioningMessage.PROVISIONING_30)
|
||||
|
||||
def testNewSessionSuccess(self):
|
||||
test_data_utility.AddDrmIntermediateCertificateWithTestData(
|
||||
@@ -77,7 +75,7 @@ class NewSessionTest(unittest.TestCase):
|
||||
test_data_utility.AddDrmIntermediateCertificateWithTestData(
|
||||
self._engine, 2001, verify_success=True)
|
||||
(_, session_status) = self._engine.NewProvisioningSession(
|
||||
self._prov30, 'INVALID_PUBLIC_KEY',
|
||||
'INVALID_PUBLIC_KEY',
|
||||
self._data_provider.device_private_key)
|
||||
self.assertEqual(ProvisioningStatus.INVALID_DRM_DEVICE_PUBLIC_KEY,
|
||||
session_status)
|
||||
@@ -86,19 +84,11 @@ class NewSessionTest(unittest.TestCase):
|
||||
test_data_utility.AddDrmIntermediateCertificateWithTestData(
|
||||
self._engine, 2001, verify_success=True)
|
||||
(_, session_status) = self._engine.NewProvisioningSession(
|
||||
self._prov30, self._data_provider.device_public_key,
|
||||
self._data_provider.device_public_key,
|
||||
'INVALID_PRIVATE_KEY')
|
||||
self.assertEqual(ProvisioningStatus.INVALID_DRM_DEVICE_PRIVATE_KEY,
|
||||
session_status)
|
||||
|
||||
def testNewSessionInvalidProtocol(self):
|
||||
test_data_utility.AddDrmIntermediateCertificateWithTestData(
|
||||
self._engine, 2001, verify_success=True)
|
||||
(_, session_status) = self._engine.NewProvisioningSession(
|
||||
1234, self._data_provider.device_public_key,
|
||||
self._data_provider.device_private_key)
|
||||
self.assertEqual(ProvisioningStatus.INVALID_PROTOCOL, session_status)
|
||||
|
||||
def _VerifyMessageSignature(self, public_key, signed_response):
|
||||
crypto_utility.VerifySignature(public_key, signed_response.signature,
|
||||
signed_response.message)
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
from "common/python/certificate_type.h" import *
|
||||
from "provisioning_sdk/public/python/provisioning_status.h" import *
|
||||
from "provisioning_sdk/public/python/provisioning_session.h" import *
|
||||
from "protos/public/certificate_provisioning_pyclif.h" import *
|
||||
|
||||
from "provisioning_sdk/public/provisioning_engine.h":
|
||||
namespace `widevine`:
|
||||
@@ -35,7 +34,6 @@ from "provisioning_sdk/public/provisioning_engine.h":
|
||||
cert_private_key: bytes,
|
||||
cert_private_key_passhprase: bytes) -> ProvisioningStatus
|
||||
def NewProvisioningSession(self,
|
||||
protocol: SignedProvisioningMessage.ProtocolVersion,
|
||||
device_public_key: bytes,
|
||||
device_private_key: bytes) -> (new_session: ProvisioningSession,
|
||||
status: ProvisioningStatus)
|
||||
|
||||
@@ -26,8 +26,6 @@ if __name__ == '__main__':
|
||||
'%s/clif/python/runtime.cc' % common.CLIF_PREFIX,
|
||||
'%s/clif/python/slots.cc' % common.CLIF_PREFIX,
|
||||
'%s/clif/python/types.cc' % common.CLIF_PREFIX,
|
||||
'%s/certificate_provisioning_pyclif.cc' %
|
||||
common.WVPROTO_SRC_DIR,
|
||||
],
|
||||
include_dirs=[
|
||||
common.SDK_ROOT_DIR, common.GEN_DIR, common.CLIF_PREFIX, '/'
|
||||
|
||||
@@ -9,4 +9,3 @@
|
||||
from "provisioning_sdk/public/provisioning_status.h":
|
||||
namespace `widevine`:
|
||||
enum ProvisioningStatus
|
||||
def GetProvisioningStatusMessage(status: ProvisioningStatus) -> str
|
||||
|
||||
@@ -24,8 +24,12 @@ def _GetSdkRootDir():
|
||||
|
||||
SDK_ROOT_DIR = _GetSdkRootDir()
|
||||
GEN_DIR = '%s/%s' % (SDK_ROOT_DIR, GEN_DIRNAME)
|
||||
|
||||
SDK_LIBRARY_DIR = os.path.join(SDK_ROOT_DIR, 'bazel-bin', 'provisioning_sdk',
|
||||
'public')
|
||||
if not os.path.exists(SDK_LIBRARY_DIR):
|
||||
SDK_LIBRARY_DIR = SDK_ROOT_DIR
|
||||
|
||||
CLIF_PREFIX = os.environ['CLIF_PREFIX']
|
||||
BUILD_DIR = os.environ['PYEXT_BUILD_DIR']
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Lint as: python2, python3
|
||||
################################################################################
|
||||
# Copyright 2017 Google LLC.
|
||||
#
|
||||
@@ -7,6 +8,10 @@
|
||||
################################################################################
|
||||
"""Class that provides test data for Provisioning SDK testing."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
from certificate_type import CertificateType
|
||||
|
||||
@@ -34,11 +39,11 @@ class TestDataProvider(object):
|
||||
current_dir = os.path.dirname(current_dir)
|
||||
filename = os.path.join(current_dir, subfolder_path, filename)
|
||||
try:
|
||||
with open(filename, 'r') as data_file:
|
||||
with open(filename, 'rb') as data_file:
|
||||
data = data_file.read()
|
||||
return data
|
||||
except IOError:
|
||||
print 'TestDataProvider: Failed to read \'%s\'' % filename
|
||||
print('TestDataProvider: Failed to read \'%s\'' % filename)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -170,8 +170,8 @@ def NewProvisioningSessionWithTestData(
|
||||
'sample device public and private keys.')
|
||||
data_provider = test_data_provider.TestDataProvider(cert_type)
|
||||
new_session, status = engine.NewProvisioningSession(
|
||||
certificate_provisioning_pb2.SignedProvisioningMessage.PROVISIONING_30,
|
||||
data_provider.device_public_key, data_provider.device_private_key)
|
||||
data_provider.device_public_key,
|
||||
data_provider.device_private_key)
|
||||
if verify_success:
|
||||
assert (ProvisioningStatus.OK == status), 'status = %r' % status
|
||||
|
||||
|
||||
Reference in New Issue
Block a user