mirror of
https://github.com/ElvishArtisan/rivendell.git
synced 2025-05-29 15:12:34 +02:00
2021-04-27 Fred Gleason <fredg@paravelsystems.com>
* Changed the 'RDAIRPLAY_EXIT_PASSWORD' field from 'varchar(41)' to 'varchar(48)'. * Incremented the database version to 349. * Renamed the 'RDSha1Hash()' function to 'RDSha1HashFile()'. * Added 'RDSha1HashPassword()' function in 'lib/rdhash.[cpp|h]'. * Added 'RDSha1HashCheckPassword()' function in 'lib/rdhash.[cpp|h]'. * Changed the hashing algorithm used for the Exit Password for rdairplay(1) to salted SHA1. Signed-off-by: Fred Gleason <fredg@paravelsystems.com>
This commit is contained in:
parent
05c35a208c
commit
9a65658267
@ -21631,3 +21631,12 @@
|
|||||||
* Fixed a regression in 'RDLog::create()' that threw SQL errors.
|
* Fixed a regression in 'RDLog::create()' that threw SQL errors.
|
||||||
2021-04-27 Fred Gleason <fredg@paravelsystems.com>
|
2021-04-27 Fred Gleason <fredg@paravelsystems.com>
|
||||||
* Fixed a regression in 'RDLogModel' that threw SQL errors.
|
* Fixed a regression in 'RDLogModel' that threw SQL errors.
|
||||||
|
2021-04-27 Fred Gleason <fredg@paravelsystems.com>
|
||||||
|
* Changed the 'RDAIRPLAY_EXIT_PASSWORD' field from 'varchar(41)'
|
||||||
|
to 'varchar(48)'.
|
||||||
|
* Incremented the database version to 349.
|
||||||
|
* Renamed the 'RDSha1Hash()' function to 'RDSha1HashFile()'.
|
||||||
|
* Added 'RDSha1HashPassword()' function in 'lib/rdhash.[cpp|h]'.
|
||||||
|
* Added 'RDSha1HashCheckPassword()' function in 'lib/rdhash.[cpp|h]'.
|
||||||
|
* Changed the hashing algorithm used for the Exit Password for
|
||||||
|
rdairplay(1) to salted SHA1.
|
||||||
|
@ -26,7 +26,7 @@ DEFAULT_SERVICE varchar(10) From SERVICES.NAME
|
|||||||
HOUR_SELECTOR_ENABLED enum('N','Y')
|
HOUR_SELECTOR_ENABLED enum('N','Y')
|
||||||
EXIT_CODE int(11) 0=clean, 1=dirty
|
EXIT_CODE int(11) 0=clean, 1=dirty
|
||||||
VIRTUAL_EXIT_CODE int(11) 0=clean, 1=dirty
|
VIRTUAL_EXIT_CODE int(11) 0=clean, 1=dirty
|
||||||
EXIT_PASSWORD varchar(41)
|
EXIT_PASSWORD varchar(48)
|
||||||
SKIN_PATH varchar(191)
|
SKIN_PATH varchar(191)
|
||||||
SHOW_COUNTERS enum('N','Y')
|
SHOW_COUNTERS enum('N','Y')
|
||||||
AUDITION_PREROLL int(11)
|
AUDITION_PREROLL int(11)
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
/*
|
/*
|
||||||
* Current Database Version
|
* Current Database Version
|
||||||
*/
|
*/
|
||||||
#define RD_VERSION_DATABASE 348
|
#define RD_VERSION_DATABASE 349
|
||||||
|
|
||||||
|
|
||||||
#endif // DBVERSION_H
|
#endif // DBVERSION_H
|
||||||
|
@ -20,10 +20,11 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "rdairplay_conf.h"
|
||||||
#include "rddb.h"
|
#include "rddb.h"
|
||||||
#include "rdconf.h"
|
#include "rdconf.h"
|
||||||
#include "rdairplay_conf.h"
|
|
||||||
#include "rdescape_string.h"
|
#include "rdescape_string.h"
|
||||||
|
#include "rdhash.h"
|
||||||
|
|
||||||
RDAirPlayConf::RDAirPlayConf(const QString &station,const QString &tablename)
|
RDAirPlayConf::RDAirPlayConf(const QString &station,const QString &tablename)
|
||||||
{
|
{
|
||||||
@ -662,34 +663,40 @@ bool RDAirPlayConf::exitPasswordValid(const QString &passwd) const
|
|||||||
{
|
{
|
||||||
QString sql;
|
QString sql;
|
||||||
RDSqlQuery *q;
|
RDSqlQuery *q;
|
||||||
|
bool ret=false;
|
||||||
sql=QString("select `EXIT_PASSWORD` from `")+air_tablename+"` where "+
|
|
||||||
"STATION='"+RDEscapeString(air_station)+"' && "+
|
sql=QString("select ")+
|
||||||
"((`EXIT_PASSWORD`=PASSWORD('"+RDEscapeString(passwd)+"'))";
|
"`EXIT_PASSWORD` "+ // 00
|
||||||
if(passwd.isEmpty()) {
|
"from `"+air_tablename+"` where "+
|
||||||
sql+="||(`EXIT_PASSWORD` is null)";
|
"`STATION`='"+RDEscapeString(air_station)+"'";
|
||||||
}
|
|
||||||
sql+=")";
|
|
||||||
q=new RDSqlQuery(sql);
|
q=new RDSqlQuery(sql);
|
||||||
if(q->size()>0) {
|
if(q->first()) {
|
||||||
delete q;
|
if(passwd.isEmpty()) {
|
||||||
return true;
|
ret=q->value(0).isNull();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret=RDSha1HashCheckPassword(passwd,q->value(0).toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delete q;
|
return ret;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void RDAirPlayConf::setExitPassword(const QString &passwd) const
|
void RDAirPlayConf::setExitPassword(const QString &passwd) const
|
||||||
{
|
{
|
||||||
QString sql;
|
QString sql;
|
||||||
RDSqlQuery *q;
|
|
||||||
|
|
||||||
sql=QString("update `")+air_tablename+"` set "+
|
if(passwd.isEmpty()) {
|
||||||
"`EXIT_PASSWORD`=PASSWORD('"+RDEscapeString(passwd)+"') where "+
|
sql=QString("update `")+air_tablename+"` set "+
|
||||||
"`STATION`='"+RDEscapeString(air_station)+"'";
|
"`EXIT_PASSWORD`=null where "+
|
||||||
q=new RDSqlQuery(sql);
|
"`STATION`='"+RDEscapeString(air_station)+"'";
|
||||||
delete q;
|
}
|
||||||
|
else {
|
||||||
|
sql=QString("update `")+air_tablename+"` set "+
|
||||||
|
"`EXIT_PASSWORD`='"+RDEscapeString(RDSha1HashPassword(passwd))+"' where "+
|
||||||
|
"`STATION`='"+RDEscapeString(air_station)+"'";
|
||||||
|
}
|
||||||
|
RDSqlQuery::apply(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,9 +27,28 @@
|
|||||||
|
|
||||||
#include <openssl/sha.h>
|
#include <openssl/sha.h>
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
#include "rdhash.h"
|
#include "rdhash.h"
|
||||||
|
|
||||||
QString RDSha1Hash(const QString &filename,bool throttle)
|
QString __RDSha1Hash_MakePasswordHash(const QString &secret,const QString &salt)
|
||||||
|
{
|
||||||
|
SHA_CTX ctx;
|
||||||
|
unsigned char md[SHA_DIGEST_LENGTH];
|
||||||
|
|
||||||
|
SHA1_Init(&ctx);
|
||||||
|
SHA1_Update(&ctx,salt.toUtf8(),salt.toUtf8().length());
|
||||||
|
SHA1_Update(&ctx,secret.toUtf8(),secret.toUtf8().length());
|
||||||
|
SHA1_Final(md,&ctx);
|
||||||
|
QString ret=salt;
|
||||||
|
for(int i=0;i<SHA_DIGEST_LENGTH;i++) {
|
||||||
|
ret+=QString().sprintf("%02x",0xff&md[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RDSha1HashFile(const QString &filename,bool throttle)
|
||||||
{
|
{
|
||||||
QString ret;
|
QString ret;
|
||||||
SHA_CTX ctx;
|
SHA_CTX ctx;
|
||||||
@ -57,3 +76,27 @@ QString RDSha1Hash(const QString &filename,bool throttle)
|
|||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
QString RDSha1HashPassword(const QString &secret)
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a salt value
|
||||||
|
//
|
||||||
|
srand(QDateTime::currentDateTime().toMSecsSinceEpoch());
|
||||||
|
QString salt=QString().sprintf("%08x",rand());
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate the hash
|
||||||
|
//
|
||||||
|
return __RDSha1Hash_MakePasswordHash(secret,salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool RDSha1HashCheckPassword(const QString &secret,const QString &hash)
|
||||||
|
{
|
||||||
|
QString salt=secret.left(8);
|
||||||
|
|
||||||
|
return __RDSha1Hash_MakePasswordHash(secret,hash.left(8))==hash;
|
||||||
|
}
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
|
|
||||||
#include <qstring.h>
|
#include <qstring.h>
|
||||||
|
|
||||||
QString RDSha1Hash(const QString &filename,bool throttle=false);
|
QString RDSha1HashFile(const QString &filename,bool throttle=false);
|
||||||
|
QString RDSha1HashPassword(const QString &secret);
|
||||||
|
bool RDSha1HashCheckPassword(const QString &secret,const QString &hash);
|
||||||
|
|
||||||
|
|
||||||
#endif // RD_H
|
#endif // RDHASH_H
|
||||||
|
@ -2334,7 +2334,7 @@ void MainObject::CheckInRecording(QString cutname,CatchEvent *evt,
|
|||||||
s->setChannels(evt->channels());
|
s->setChannels(evt->channels());
|
||||||
cut->checkInRecording(rda->config()->stationName(),"",
|
cut->checkInRecording(rda->config()->stationName(),"",
|
||||||
rda->config()->stationName(),s,msecs);
|
rda->config()->stationName(),s,msecs);
|
||||||
cut->setSha1Hash(RDSha1Hash(RDCut::pathName(cut->cutName())));
|
cut->setSha1Hash(RDSha1HashFile(RDCut::pathName(cut->cutName())));
|
||||||
delete s;
|
delete s;
|
||||||
cut->autoTrim(RDCut::AudioBoth,-threshold);
|
cut->autoTrim(RDCut::AudioBoth,-threshold);
|
||||||
RDCart *cart=new RDCart(cut->cartNumber());
|
RDCart *cart=new RDCart(cut->cartNumber());
|
||||||
|
@ -133,6 +133,7 @@ MainWidget::MainWidget(RDConfig *c,QWidget *parent)
|
|||||||
login_button=new QPushButton(this);
|
login_button=new QPushButton(this);
|
||||||
login_button->setFont(buttonFont());
|
login_button->setFont(buttonFont());
|
||||||
login_button->setText(tr("Set User"));
|
login_button->setText(tr("Set User"));
|
||||||
|
login_button->setDefault(true);
|
||||||
connect(login_button,SIGNAL(clicked()),this,SLOT(loginData()));
|
connect(login_button,SIGNAL(clicked()),this,SLOT(loginData()));
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -32,7 +32,9 @@ MainObject::MainObject(QObject *parent)
|
|||||||
:QObject(parent)
|
:QObject(parent)
|
||||||
{
|
{
|
||||||
QString filename="";
|
QString filename="";
|
||||||
|
QString password="";
|
||||||
|
QString hash="";
|
||||||
|
|
||||||
//
|
//
|
||||||
// Read Command Options
|
// Read Command Options
|
||||||
//
|
//
|
||||||
@ -42,27 +44,58 @@ MainObject::MainObject(QObject *parent)
|
|||||||
filename=cmd->value(i);
|
filename=cmd->value(i);
|
||||||
cmd->setProcessed(i,true);
|
cmd->setProcessed(i,true);
|
||||||
}
|
}
|
||||||
|
if(cmd->key(i)=="--hash") {
|
||||||
|
hash=cmd->value(i);
|
||||||
|
cmd->setProcessed(i,true);
|
||||||
|
}
|
||||||
|
if(cmd->key(i)=="--password") {
|
||||||
|
password=cmd->value(i);
|
||||||
|
cmd->setProcessed(i,true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(filename.isEmpty()) {
|
if(filename.isEmpty()&&password.isEmpty()) {
|
||||||
fprintf(stderr,"test_hash: missing --filename\n");
|
fprintf(stderr,"test_hash: missing --filename or --password\n");
|
||||||
exit(256);
|
exit(256);
|
||||||
}
|
}
|
||||||
|
if((!filename.isEmpty())&&(!password.isEmpty())) {
|
||||||
QString hash=RDSha1Hash(filename);
|
fprintf(stderr,"test_hash: --filename and --password are mutually exclusive\n");
|
||||||
if(hash.isEmpty()) {
|
|
||||||
fprintf(stderr,"test_hash: unable to open \"%s\"\n",
|
|
||||||
filename.toUtf8().constData());
|
|
||||||
exit(256);
|
exit(256);
|
||||||
}
|
}
|
||||||
printf("%s\n",hash.toUtf8().constData());
|
|
||||||
|
|
||||||
exit(0);
|
if(!filename.isEmpty()) { // Hash the specified file
|
||||||
|
hash=RDSha1HashFile(filename);
|
||||||
|
if(hash.isEmpty()) {
|
||||||
|
fprintf(stderr,"test_hash: unable to open \"%s\"\n",
|
||||||
|
filename.toUtf8().constData());
|
||||||
|
exit(256);
|
||||||
|
}
|
||||||
|
printf("%s\n",hash.toUtf8().constData());
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if((!hash.isEmpty())&&(!password.isEmpty())) { // Check the specified hash
|
||||||
|
if( RDSha1HashCheckPassword(password,hash)) {
|
||||||
|
printf("Match!\n");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
printf("No Match!\n");
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!password.isEmpty()) { // Generate password hash
|
||||||
|
printf("%s\n",RDSha1HashPassword(password).toUtf8().constData());
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr,"test_hash: inconsistent arguments given!\n");
|
||||||
|
exit(256);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(int argc,char *argv[])
|
int main(int argc,char *argv[])
|
||||||
{
|
{
|
||||||
QApplication a(argc,argv,false);
|
QCoreApplication a(argc,argv);
|
||||||
new MainObject();
|
new MainObject();
|
||||||
return a.exec();
|
return a.exec();
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
#include <rdcmd_switch.cpp>
|
#include <rdcmd_switch.cpp>
|
||||||
#include <rdhash.h>
|
#include <rdhash.h>
|
||||||
|
|
||||||
#define TEST_HASH_USAGE "[options]\n\nTest SHA1 has generation\n\n--filename=<file-name>\n The name of the file for which to generate a hash.\n\n"
|
#define TEST_HASH_USAGE "[options]\n\nTest SHA1 hash generation\n\n--filename=<file-name>\n The name of the file for which to generate a hash.\n\n--password=<secret>\n Generate a password hash from <secret>\n\n--hash=<hash>\n When given with --secret, verify <hash>\n\n"
|
||||||
|
|
||||||
class MainObject : public QObject
|
class MainObject : public QObject
|
||||||
{
|
{
|
||||||
|
@ -211,7 +211,7 @@ void MainObject::CheckTableAttributes()
|
|||||||
printf(" Database uses default charset/collation %s/%s, should be utf8mb4/%s. Fix? (y/N) ",
|
printf(" Database uses default charset/collation %s/%s, should be utf8mb4/%s. Fix? (y/N) ",
|
||||||
q->value(1).toString().toUtf8().constData(),
|
q->value(1).toString().toUtf8().constData(),
|
||||||
q->value(2).toString().toUtf8().constData(),
|
q->value(2).toString().toUtf8().constData(),
|
||||||
db_config->mysqlCollation());
|
db_config->mysqlCollation().toUtf8().constData());
|
||||||
fflush(NULL);
|
fflush(NULL);
|
||||||
if(UserResponse()) {
|
if(UserResponse()) {
|
||||||
sql=QString("alter database `")+db_mysql_database+"` "+
|
sql=QString("alter database `")+db_mysql_database+"` "+
|
||||||
@ -400,7 +400,7 @@ void MainObject::RelinkAudio(const QString &srcdir) const
|
|||||||
QStringList files=dir.entryList(QDir::Files|QDir::Readable|QDir::Hidden);
|
QStringList files=dir.entryList(QDir::Files|QDir::Readable|QDir::Hidden);
|
||||||
for(int i=0;i<files.size();i++) {
|
for(int i=0;i<files.size();i++) {
|
||||||
QString filename=dir.path()+"/"+files[i];
|
QString filename=dir.path()+"/"+files[i];
|
||||||
QString hash=RDSha1Hash(filename);
|
QString hash=RDSha1HashFile(filename);
|
||||||
QString firstdest;
|
QString firstdest;
|
||||||
bool delete_source=true;
|
bool delete_source=true;
|
||||||
|
|
||||||
@ -913,7 +913,7 @@ void MainObject::RehashCart(unsigned cartnum) const
|
|||||||
|
|
||||||
void MainObject::RehashCut(const QString &cutnum) const
|
void MainObject::RehashCut(const QString &cutnum) const
|
||||||
{
|
{
|
||||||
QString hash=RDSha1Hash(RDCut::pathName(cutnum),true);
|
QString hash=RDSha1HashFile(RDCut::pathName(cutnum),true);
|
||||||
if(hash.isEmpty()) {
|
if(hash.isEmpty()) {
|
||||||
printf(" Unable to generate hash for \"%s\"\n",
|
printf(" Unable to generate hash for \"%s\"\n",
|
||||||
RDCut::pathName(cutnum).toUtf8().constData());
|
RDCut::pathName(cutnum).toUtf8().constData());
|
||||||
|
@ -40,6 +40,19 @@ bool MainObject::RevertSchema(int cur_schema,int set_schema,QString *err_msg)
|
|||||||
// NEW SCHEMA REVERSIONS GO HERE...
|
// NEW SCHEMA REVERSIONS GO HERE...
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Revert 349
|
||||||
|
//
|
||||||
|
if((cur_schema==349)&&(set_schema<cur_schema)) {
|
||||||
|
sql=QString("alter table `RDAIRPLAY` ")+
|
||||||
|
"modify column EXIT_PASSWORD varchar(41)";
|
||||||
|
if(!RDSqlQuery::apply(sql,err_msg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSchemaVersion(--cur_schema);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Revert 348
|
// Revert 348
|
||||||
//
|
//
|
||||||
|
@ -159,7 +159,7 @@ void MainObject::InitializeSchemaMap() {
|
|||||||
global_version_map["3.3"]=314;
|
global_version_map["3.3"]=314;
|
||||||
global_version_map["3.4"]=317;
|
global_version_map["3.4"]=317;
|
||||||
global_version_map["3.5"]=346;
|
global_version_map["3.5"]=346;
|
||||||
global_version_map["4.0"]=348;
|
global_version_map["4.0"]=349;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -10774,6 +10774,16 @@ bool MainObject::UpdateSchema(int cur_schema,int set_schema,QString *err_msg)
|
|||||||
WriteSchemaVersion(++cur_schema);
|
WriteSchemaVersion(++cur_schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if((cur_schema<349)&&(set_schema>cur_schema)) {
|
||||||
|
sql=QString("alter table `RDAIRPLAY` ")+
|
||||||
|
"modify column EXIT_PASSWORD varchar(48)";
|
||||||
|
if(!RDSqlQuery::apply(sql,err_msg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSchemaVersion(++cur_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// NEW SCHEMA UPDATES GO HERE...
|
// NEW SCHEMA UPDATES GO HERE...
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ void Xport::Import()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(resp_code==200) {
|
if(resp_code==200) {
|
||||||
cut->setSha1Hash(RDSha1Hash(RDCut::pathName(cut->cutName())));
|
cut->setSha1Hash(RDSha1HashFile(RDCut::pathName(cut->cutName())));
|
||||||
if(!title.isEmpty()) {
|
if(!title.isEmpty()) {
|
||||||
cart->setTitle(title);
|
cart->setTitle(title);
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ void Xport::SavePodcast()
|
|||||||
delete cast;
|
delete cast;
|
||||||
XmlExit(err_msg.toUtf8(),500,"podcasts.cpp",LINE_NUMBER);
|
XmlExit(err_msg.toUtf8(),500,"podcasts.cpp",LINE_NUMBER);
|
||||||
}
|
}
|
||||||
cast->setSha1Hash(RDSha1Hash(destpath));
|
cast->setSha1Hash(RDSha1HashFile(destpath));
|
||||||
|
|
||||||
printf("Content-type: text/html; charset: UTF-8\n");
|
printf("Content-type: text/html; charset: UTF-8\n");
|
||||||
printf("Status: 200\n\n");
|
printf("Status: 200\n\n");
|
||||||
@ -172,7 +172,7 @@ void Xport::GetPodcast()
|
|||||||
data=new char[st.st_blksize];
|
data=new char[st.st_blksize];
|
||||||
n=read(fd,data,st.st_blksize);
|
n=read(fd,data,st.st_blksize);
|
||||||
while(n>0) {
|
while(n>0) {
|
||||||
write(1,data,n);
|
n=write(1,data,n);
|
||||||
n=read(fd,data,st.st_blksize);
|
n=read(fd,data,st.st_blksize);
|
||||||
}
|
}
|
||||||
delete data;
|
delete data;
|
||||||
|
@ -59,7 +59,7 @@ void Xport::Rehash()
|
|||||||
delete cut;
|
delete cut;
|
||||||
XmlExit("No such cut",404,"rdhash.cpp",LINE_NUMBER);
|
XmlExit("No such cut",404,"rdhash.cpp",LINE_NUMBER);
|
||||||
}
|
}
|
||||||
cut->setSha1Hash(RDSha1Hash(RDCut::pathName(cart_number,cut_number)));
|
cut->setSha1Hash(RDSha1HashFile(RDCut::pathName(cart_number,cut_number)));
|
||||||
delete cut;
|
delete cut;
|
||||||
XmlExit("OK",200,"rdhash.cpp",LINE_NUMBER);
|
XmlExit("OK",200,"rdhash.cpp",LINE_NUMBER);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user