New files_antivir Version 4.0-1
This commit is contained in:
Родитель
78ced6b5d5
Коммит
d1564dcc71
|
@ -13,6 +13,7 @@ remove the file if it's infected.
|
|||
|
||||
The App is not complete yet, the following works/is done:
|
||||
* It can be configured to work with the executable or the daemon mode of ClamAV
|
||||
* If used in daemon mode it can connect through network- or local file-socket
|
||||
* In daemon mode, it sends files to a remote/local server using INSTREAM command
|
||||
* When the user uploads a file, it's checked
|
||||
* If an uploaded file is infected, it's deleted and a notification is shown to the user on screen and an email is sent with details.
|
||||
|
@ -35,6 +36,10 @@ The App is not complete yet, the following works/is done:
|
|||
* Owncloud 4
|
||||
* ClamAV (Binaries or a server running ClamAV in daemon mode)
|
||||
|
||||
## 3rd party software used
|
||||
* Simplesocketclient for connecting to ClamAV file-socket: http://github.com/kijin/simplesocket
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
* Install and enable the App
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<info>
|
||||
<id>files_antivirus</id>
|
||||
<name>Antivirus App for files</name>
|
||||
<version>0.4</version>
|
||||
<version>0.4-1</version>
|
||||
<description>Verify files for virus using ClamAV</description>
|
||||
<licence>AGPL</licence>
|
||||
<author>Manuel Delgado, Bart Visscher</author>
|
||||
<author>Manuel Delgado, Bart Visscher, thinksilicon.de</author>
|
||||
<require>5</require>
|
||||
<types>
|
||||
<filesystem/>
|
||||
|
|
30
l10n/de.php
30
l10n/de.php
|
@ -1,24 +1,26 @@
|
|||
<?php $TRANSLATIONS = array(
|
||||
"Greetings {user}," => "Herzlich Willkommen {user},",
|
||||
"Sorry, but a malware was detected in a file you tried to upload and it had to be deleted." => "Entschuldige, es wurde Malware in einer Datei gefunden die Du hochladen wolltest und sie musste gelöscht werden.",
|
||||
"This email is a notification from {host}. Please, do not reply." => "Diese E-Mail ist eine Benachrichtigung von {host}. Bitte nicht antworten.",
|
||||
"File uploaded: {file}" => "Datei {file} hochgeladen.",
|
||||
"Sorry, but a malware was detected in a file you tried to upload and it had to be deleted." => "Entschuldigung, in einer Datei, die Sie hochladen wollten, wurde Malware gefunden und sie musste daher gelöscht werden.",
|
||||
"This email is a notification from {host}. Please, do not reply." => "Diese E-Mail ist eine Benachrichtigung von {host}. Bitte antworten Sie nicht.",
|
||||
"File uploaded: {file}" => "Datei wurde hochgeladen: {file}",
|
||||
"Antivirus Configuration" => "Antivirus-Konfiguration",
|
||||
"Mode" => "Modus",
|
||||
"Executable" => "ausführbar",
|
||||
"Daemon" => "Dienst",
|
||||
"Executable" => "Ausführbar",
|
||||
"Daemon" => "Dienst (Daemon)",
|
||||
"Daemon (Socket)" => "Dienst (Socket)",
|
||||
"Host" => "Host",
|
||||
"Address of Antivirus Host." => "Adresse des Antivirus-Servers",
|
||||
"Not required in Executable Mode." => "Nicht benötigt im ausführbaren Modus",
|
||||
"Address of Antivirus Host." => "Die Adresse des Antivirus-Hosts.",
|
||||
"Not required in Executable Mode." => "Nicht erforderlich im ausführbaren Modus.",
|
||||
"Port" => "Port",
|
||||
"Port number of Antivirus Host." => "Port des Antivirus-Servers",
|
||||
"Stream Length" => "Stream-Länge",
|
||||
"ClamAV StreamMaxLength value in bytes." => "ClamAV-Wert 'StreamMaxLength' in Bytes.",
|
||||
"Port number of Antivirus Host." => "Die Portnummer des Antivirus-Hosts.",
|
||||
"Stream Length" => "Übertragungslänge",
|
||||
"ClamAV StreamMaxLength value in bytes." => "ClamAV StreamMaxLength-Wert in Bytes.",
|
||||
"Path to clamscan" => "Pfad zu clamscan",
|
||||
"Path to clamscan executable." => "Pfad zur clamscan Datei",
|
||||
"Not required in Daemon Mode." => "Nicht erforderlich im Dienst Modus",
|
||||
"Action for infected files found while scanning" => "Aktion für infizierte Dateien die beim Scannen gefunden werden",
|
||||
"Only log" => "Nur protokollieren",
|
||||
"Path to clamscan executable." => "Pfad zum clamscan-Programm",
|
||||
"ClamAV Socket" => "Pfad zum ClamAV-Socket",
|
||||
"Not required in Daemon Mode." => "Nicht erforderlich im Dienstmodus (Daemon)",
|
||||
"Action for infected files found while scanning" => "Aktion für infizierte Dateien, welche beim Scannen gefunden wurden",
|
||||
"Only log" => "Nur loggen",
|
||||
"Delete file" => "Datei löschen",
|
||||
"Save" => "Speichern"
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"Mode" => "Modus",
|
||||
"Executable" => "Ausführbar",
|
||||
"Daemon" => "Dienst (Daemon)",
|
||||
"Daemon (Socket)" => "Dienst (Socket)",
|
||||
"Host" => "Host",
|
||||
"Address of Antivirus Host." => "Die Adresse des Antivirus-Hosts.",
|
||||
"Not required in Executable Mode." => "Nicht erforderlich im ausführbaren Modus.",
|
||||
|
@ -16,6 +17,7 @@
|
|||
"ClamAV StreamMaxLength value in bytes." => "ClamAV StreamMaxLength-Wert in Bytes.",
|
||||
"Path to clamscan" => "Pfad zu clamscan",
|
||||
"Path to clamscan executable." => "Pfad zum clamscan-Programm",
|
||||
"ClamAV Socket" => "Pfad zum ClamAV-Socket",
|
||||
"Not required in Daemon Mode." => "Nicht erforderlich im Dienstmodus (Daemon)",
|
||||
"Action for infected files found while scanning" => "Aktion für infizierte Dateien, welche beim Scannen gefunden wurden",
|
||||
"Only log" => "Nur loggen",
|
||||
|
|
|
@ -75,6 +75,8 @@ class OC_Files_Antivirus {
|
|||
return self::_clamav_scan_via_daemon($filepath);
|
||||
case 'executable':
|
||||
return self::_clamav_scan_via_exec($filepath);
|
||||
case 'socket':
|
||||
return self::_clamav_scan_via_socket($filepath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,4 +196,45 @@ class OC_Files_Antivirus {
|
|||
return CLAMAV_SCANRESULT_UNCHECKED;
|
||||
}
|
||||
}
|
||||
private static function _clamav_scan_via_socket( $filepath ) {
|
||||
$av_socket = \OCP\Config::getAppValue( 'files_antivirus', 'av_socket', '' );
|
||||
require_once( 'simplesocketclient.php' );
|
||||
$socket = new SimpleSocketClient( $av_socket, false, 5 );
|
||||
if( $socket->connect() === false ) {
|
||||
\OCP\Util::writeLog( 'files_antivirus', 'Could not connect to Clamd via socket '.$av_socket.'!', \OCP\Util::ERROR );
|
||||
return CLAMAV_SCANRESULT_UNCHECKED;
|
||||
}
|
||||
$socket->write('SCAN ' . $filepath );
|
||||
$response = $socket->readline();
|
||||
$response = trim($response);
|
||||
$socket->disconnect();
|
||||
|
||||
if (!strncmp($response, $filepath . ':', strlen($filepath) + 1)) {
|
||||
|
||||
// Cut the filename from the response.
|
||||
$response = substr($response, strlen($filepath) + 2);
|
||||
|
||||
// OK
|
||||
if ($response === 'OK') {
|
||||
\OCP\Util::writeLog( 'files_antivirus', 'Result CLEAN!', \OCP\Util::DEBUG );
|
||||
return CLAMAV_SCANRESULT_CLEAN;
|
||||
}
|
||||
|
||||
// FOUND
|
||||
if (substr($response, strlen($response) - 5) === 'FOUND') {
|
||||
$virus = substr($response, 0, strlen($response) - 6);
|
||||
\OCP\Util::writeLog( 'files_antivirus', 'Virus detected in file. Clamd reported '.$virus , \OCP\Util::WARN );
|
||||
return CLAMAV_SCANRESULT_INFECTED;
|
||||
}
|
||||
|
||||
// ERROR
|
||||
if (substr($response, strlen($response) - 5) === 'ERROR') {
|
||||
substr( $response, 0, strlen( $response ) - 6 );
|
||||
\OCP\Util::writeLog( 'files_antivirus', 'File could not be scanned. Clamd reported '.$response, \OCP\Util::ERROR );
|
||||
return CLAMAV_SCANRESULT_UNCHECKED;
|
||||
}
|
||||
}
|
||||
\OCP\Util::writeLog( 'files_antivirus', 'No response from Clamd!', \OCP\Util::ERROR );
|
||||
return CLAMAV_SCANRESULT_UNCHECKED;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,351 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A Simple Socket Client for PHP 5.2+
|
||||
*
|
||||
* This class implements a generic socket client. It can be extended to
|
||||
* create client libraries working with text-based protocols.
|
||||
* In addition to socket creation, reading, and writing capabilities,
|
||||
* this class also supports lazy loading (not connect until actually needed),
|
||||
* as well as basic key validation and command building helper methods.
|
||||
*
|
||||
* URL: http://github.com/kijin/simplesocket
|
||||
* Version: 0.1.8
|
||||
*
|
||||
* Copyright (c) 2010-2011, Kijin Sung <kijin.sung@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
class SimpleSocketClient
|
||||
{
|
||||
/**
|
||||
* Connection settings.
|
||||
*
|
||||
* Although these properties can be manipulated directly by children,
|
||||
* it is best to keep them in the hands of the parent class.
|
||||
*/
|
||||
|
||||
protected $_con = null;
|
||||
protected $_host = '';
|
||||
protected $_port = 0;
|
||||
protected $_timeout = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Default host and port.
|
||||
*
|
||||
* These properties can be overridden by children to provide default values
|
||||
* for host and port if they're not passed to the constructor.
|
||||
*/
|
||||
|
||||
protected $_default_host = null;
|
||||
protected $_default_port = null;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Host and port must be supplied at the time of instantiation.
|
||||
*
|
||||
* @param string The hostname, IP address, or UNIX socket for the server.
|
||||
* @param int The port of the server, or false for UNIX sockets.
|
||||
* @param int Connection timeout in seconds. [optional: default is 5]
|
||||
*/
|
||||
|
||||
public function __construct($host = null, $port = null, $timeout = 5)
|
||||
{
|
||||
// A quick check for IPv6 addresses. (They contain colons.)
|
||||
|
||||
if (strpos($host, ':') !== false && strpos($host, '[') === false)
|
||||
{
|
||||
$host = '[' . $host . ']';
|
||||
}
|
||||
|
||||
// Use default values?
|
||||
|
||||
if (is_null($host) && is_null($port))
|
||||
{
|
||||
$host = $this->_default_host;
|
||||
$port = $this->_default_port;
|
||||
}
|
||||
|
||||
// Keep the connection info, but don't connect now.
|
||||
|
||||
$this->_host = $host;
|
||||
$this->_port = $port;
|
||||
$this->_timeout = $timeout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Connect to the server.
|
||||
*
|
||||
* Normally, this method is useful only for debugging purposes, because
|
||||
* it will be called automatically the first time a read/write operation
|
||||
* is attempted.
|
||||
*/
|
||||
|
||||
public function connect()
|
||||
{
|
||||
// If already connected, don't do anything.
|
||||
|
||||
if ($this->_con !== null && $this->_con !== false) return true;
|
||||
|
||||
// If a previous connection attempt had failed, do not retry.
|
||||
|
||||
if ($this->_con === false) throw new SimpleSocketException('Cannot connect to ' . $this->_host . ' port ' . $this->_port);
|
||||
|
||||
// Attempt to connect.
|
||||
|
||||
$socket = $this->_port ? ($this->_host . ':' . $this->_port) : ('unix://' . $this->_host);
|
||||
$this->_con = stream_socket_client($socket, $errno, $errstr, $this->_timeout);
|
||||
|
||||
// If there's an error, set $_con to false, and throw an exception.
|
||||
|
||||
if (!$this->_con)
|
||||
{
|
||||
$this->_con = false;
|
||||
throw new SimpleSocketException('Cannot connect to ' . $this->_host . ' port ' . $this->_port . ': ' . $errstr . ' (code ' . $errno . ')');
|
||||
}
|
||||
|
||||
// Return true to indicate success.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from the server.
|
||||
*
|
||||
* Normally, this method is useful only for debugging purposes, because
|
||||
* it will be automatically called in the event of an error (resulting in
|
||||
* reconnection the next time a read/write operation is attempted), as
|
||||
* well as at the end of the execution of the script.
|
||||
*/
|
||||
|
||||
public function disconnect()
|
||||
{
|
||||
// Close the socket.
|
||||
|
||||
@fclose($this->_con);
|
||||
$this->_con = null;
|
||||
|
||||
// Return true to indicate success.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic read method.
|
||||
*
|
||||
* This method reads a specified number of bytes from the socket.
|
||||
* By default, it will also read a CRLF sequence (2 bytes) in addition to
|
||||
* the specified number of bytes, and remove that CRLF sequence once it
|
||||
* has been read. This is useful for most text-based protocols; however,
|
||||
* if you do not want such behavior, pass additional 'false' arguments.
|
||||
*
|
||||
* @param int The number of bytes to read, or -1 to read until EOF.
|
||||
* @param bool Whether or not to read CRLF at the end, too. [optional: default is true]
|
||||
* @return string Data read from the socket.
|
||||
* @throws Exception If an error occurs while reading from the socket.
|
||||
*/
|
||||
|
||||
public function read($bytes = -1, $autonewline = true)
|
||||
{
|
||||
// If not connected yet, connect now.
|
||||
|
||||
if ($this->_con === null) $this->connect();
|
||||
|
||||
// Read the data from the socket.
|
||||
|
||||
$data = stream_get_contents($this->_con, $bytes);
|
||||
|
||||
// If $autonewline is true, read 2 more bytes.
|
||||
|
||||
if ($autonewline && $bytes !== -1) stream_get_contents($this->_con, 2);
|
||||
|
||||
// If the result is false, throw an exception.
|
||||
|
||||
if ($data === false)
|
||||
{
|
||||
$this->disconnect();
|
||||
throw new SimpleSocketException('Cannot read ' . $bytes . ' bytes from ' . $this->_host . ' port ' . $this->_port);
|
||||
}
|
||||
|
||||
// Otherwise, return the data.
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic readline method.
|
||||
*
|
||||
* This method reads one line from the socket, i.e. it reads until it hits
|
||||
* a CRLF sequence. By default, that CRLF sequence will be removed from the
|
||||
* return value. This is useful for most text-based protocols; however,
|
||||
* if you do not want such behavior, pass an additional 'false' argument.
|
||||
*
|
||||
* @param bool Whether or not to strip CRLF from the end of the response. [optional: default is true]
|
||||
* @return string Data read from the socket.
|
||||
* @throws Exception If an error occurs while reading from the socket.
|
||||
*/
|
||||
|
||||
public function readline($trim = true)
|
||||
{
|
||||
// If not connected yet, connect now.
|
||||
|
||||
if ($this->_con === null) $this->connect();
|
||||
|
||||
// Read a line from the socket.
|
||||
|
||||
$data = fgets($this->_con);
|
||||
|
||||
// If the result is false, throw an exception.
|
||||
|
||||
if ($data === false)
|
||||
{
|
||||
$this->disconnect();
|
||||
throw new SimpleSocketException('Cannot read a line from ' . $this->_host . ' port ' . $this->_port);
|
||||
}
|
||||
|
||||
// Otherwise, trim and return the data.
|
||||
|
||||
if ($trim && substr($data, strlen($data) - 2) === "\r\n") $data = substr($data, 0, strlen($data) - 2);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic write method.
|
||||
*
|
||||
* This method writes a string to the socket. By default, this method will
|
||||
* write a CRLF sequence in addition to the given string. This is useful
|
||||
* for most text-based protocols; however, if you do not want such behavior,
|
||||
* make sure to pass an additional 'false' argument.
|
||||
*
|
||||
* @param string The string to write to the socket.
|
||||
* @param bool Whether or write CRLF in addition to the given string. [optional: default is true]
|
||||
* @return bool True on success.
|
||||
* @throws Exception If an error occurs while reading from the socket.
|
||||
*/
|
||||
|
||||
public function write($string, $autonewline = true)
|
||||
{
|
||||
// If not connected yet, connect now.
|
||||
|
||||
if ($this->_con === null) $this->connect();
|
||||
|
||||
// If $autonewline is true, add CRLF to the content.
|
||||
|
||||
if ($autonewline) $string .= "\r\n";
|
||||
|
||||
// Write the whole string to the socket.
|
||||
|
||||
while ($string !== '')
|
||||
{
|
||||
// Start writing.
|
||||
|
||||
$written = fwrite($this->_con, $string);
|
||||
|
||||
// If the result is false, throw an exception.
|
||||
|
||||
if ($written === false)
|
||||
{
|
||||
$this->disconnect();
|
||||
throw new SimpleSocketException('Cannot write to ' . $this->_host . ' port ' . $this->_port);
|
||||
}
|
||||
|
||||
// If nothing was written, it probably means we've already done writing.
|
||||
|
||||
if ($written == 0) return true;
|
||||
|
||||
// Prepare the string for the next write.
|
||||
|
||||
$string = substr($string, $written);
|
||||
}
|
||||
|
||||
// Return true to indicate success.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic key validation method.
|
||||
*
|
||||
* This method will throw an exception if:
|
||||
* - The key is empty.
|
||||
* - The key is more than 250 bytes long.
|
||||
* - The key contains characters outside of the ASCII printable range.
|
||||
*
|
||||
* @param string The key to validate.
|
||||
* @return bool True of the key is valid.
|
||||
* @throws Exception If the key is invalid.
|
||||
*/
|
||||
|
||||
public function validate_key($key)
|
||||
{
|
||||
if ($key === '') throw new InvalidKeyException('Key is empty');
|
||||
if (strlen($key) > 250) throw new InvalidKeyException('Key is too long: ' . $key);
|
||||
if (preg_match('/[^\\x21-\\x7e]/', $key)) throw new InvalidKeyException('Illegal character in key: ' . $key);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic command building method.
|
||||
*
|
||||
* This method will accept one or more string arguments, and return them
|
||||
* all concatenated with one space between each. If this is convenient
|
||||
* for you, help yourself.
|
||||
*
|
||||
* @param string As many arguments as you wish.
|
||||
* @return string The concatenated string.
|
||||
*/
|
||||
|
||||
public function build_command( /* arguments */ )
|
||||
{
|
||||
$args = func_get_args();
|
||||
return implode(' ' , $args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*
|
||||
* Although not really necessary, the destructor will attempt to
|
||||
* disconnect in case something weird happens.
|
||||
*/
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
@fclose($this->_con);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception class.
|
||||
*/
|
||||
|
||||
class SimpleSocketException extends Exception { }
|
||||
class InvalidKeyException extends SimpleSocketException { }
|
|
@ -25,6 +25,7 @@ OCP\User::checkAdminUser();
|
|||
|
||||
$params = array(
|
||||
'av_mode' => 'executable',
|
||||
'av_socket' => '/var/run/clamav/clamd.ctl',
|
||||
'av_host' => '',
|
||||
'av_port' => '',
|
||||
'av_chunk_size' => '1024',
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
<fieldset class="personalblock">
|
||||
<legend><strong><?php p($l->t('Antivirus Configuration'));?></strong></legend>
|
||||
<p class='av_mode'><label for="av_mode"><?php p($l->t('Mode'));?></label>
|
||||
<select id="av_mode" name="av_mode"><?php print_unescaped(html_select_options(array('executable' => $l->t('Executable'), 'daemon' => $l->t('Daemon')), $_['av_mode'])) ?></select>
|
||||
<select id="av_mode" name="av_mode"><?php print_unescaped(html_select_options(array('executable' => $l->t('Executable'), 'daemon' => $l->t('Daemon'), 'socket' => $l->t('Daemon (Socket)')), $_['av_mode'])) ?></select>
|
||||
</p>
|
||||
<p class='av_socket'><label for="av_socket"><?php p($l->t('Socket'));?></label><input type="text" id="av_socket" name="av_socket" value="<?php p($_['av_socket']); ?>" title="<?php p($l->t('Clamav Socket.')).' '.$l->t('Not required in Executable Mode.'); ?>"></p>
|
||||
<p class='av_host'><label for="av_host"><?php p($l->t('Host'));?></label><input type="text" id="av_host" name="av_host" value="<?php p($_['av_host']); ?>" title="<?php p($l->t('Address of Antivirus Host.')). ' ' .$l->t('Not required in Executable Mode.');?>"></p>
|
||||
<p class='av_port'><label for="av_port"><?php p($l->t('Port'));?></label><input type="text" id="av_port" name="av_port" value="<?php p($_['av_port']); ?>" title="<?php p($l->t('Port number of Antivirus Host.')). ' ' .$l->t('Not required in Executable Mode.');?>"></p>
|
||||
<p class='av_chunk_size'><label for="av_chunk_size"><?php p($l->t('Stream Length'));?></label><input type="text" id="av_chunk_size" name="av_chunk_size" value="<?php p($_['av_chunk_size']); ?>" title="<?php p($l->t('ClamAV StreamMaxLength value in bytes.')). ' ' .$l->t('Not required in Executable Mode.');?>"> bytes</p>
|
||||
|
|
Загрузка…
Ссылка в новой задаче