Multiple signatures can now be verified.
This commit is contained in:
parent
6bbbad60f2
commit
06dae7fcf3
3
TODO
3
TODO
@ -5,4 +5,7 @@
|
|||||||
* Allow to use GTK's main loop instead of the select stuff in
|
* Allow to use GTK's main loop instead of the select stuff in
|
||||||
wait.c
|
wait.c
|
||||||
|
|
||||||
|
* a op_keylist_start should cancel a pending keylisy operation on the
|
||||||
|
same context
|
||||||
|
|
||||||
* need to close a lot of handles in w32-io.c
|
* need to close a lot of handles in w32-io.c
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
2001-02-12 Werner Koch <wk@gnupg.org>
|
||||||
|
|
||||||
|
Enhanced the signature verification, so that it can how handle
|
||||||
|
more than one signature and is able to return more information on
|
||||||
|
the signatures.
|
||||||
|
* verify.c (gpgme_get_sig_key): New.
|
||||||
|
(gpgme_get_sig_status): New.
|
||||||
|
|
||||||
|
* gpgme.h: Add stdio.h.
|
||||||
|
(GpgmeSigStat): New status DIFF.
|
||||||
|
|
||||||
2001-02-01 Werner Koch <wk@gnupg.org>
|
2001-02-01 Werner Koch <wk@gnupg.org>
|
||||||
|
|
||||||
* w32-io.c (set_synchronize): Add EVENT_MODIFY_STATE. Add Debug
|
* w32-io.c (set_synchronize): Add EVENT_MODIFY_STATE. Add Debug
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <unistd.h>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#ifndef GPGME_H
|
#ifndef GPGME_H
|
||||||
#define GPGME_H
|
#define GPGME_H
|
||||||
|
|
||||||
|
#include <stdio.h> /* for FILE * */
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
typedef long off_t;
|
typedef long off_t;
|
||||||
#else
|
#else
|
||||||
@ -103,7 +104,8 @@ typedef enum {
|
|||||||
GPGME_SIG_STAT_BAD = 2,
|
GPGME_SIG_STAT_BAD = 2,
|
||||||
GPGME_SIG_STAT_NOKEY = 3,
|
GPGME_SIG_STAT_NOKEY = 3,
|
||||||
GPGME_SIG_STAT_NOSIG = 4,
|
GPGME_SIG_STAT_NOSIG = 4,
|
||||||
GPGME_SIG_STAT_ERROR = 5
|
GPGME_SIG_STAT_ERROR = 5,
|
||||||
|
GPGME_SIG_STAT_DIFF = 6
|
||||||
} GpgmeSigStat;
|
} GpgmeSigStat;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@ -165,6 +167,12 @@ void gpgme_signers_clear (GpgmeCtx c);
|
|||||||
GpgmeError gpgme_signers_add (GpgmeCtx c, const GpgmeKey key);
|
GpgmeError gpgme_signers_add (GpgmeCtx c, const GpgmeKey key);
|
||||||
GpgmeKey gpgme_signers_enum (const GpgmeCtx c, int seq);
|
GpgmeKey gpgme_signers_enum (const GpgmeCtx c, int seq);
|
||||||
|
|
||||||
|
const char *gpgme_get_sig_status (GpgmeCtx c, int idx,
|
||||||
|
GpgmeSigStat *r_stat, time_t *r_created );
|
||||||
|
GpgmeError gpgme_get_sig_key (GpgmeCtx c, int idx, GpgmeKey *r_key);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Functions to handle recipients */
|
/* Functions to handle recipients */
|
||||||
GpgmeError gpgme_recipients_new (GpgmeRecipients *r_rset);
|
GpgmeError gpgme_recipients_new (GpgmeRecipients *r_rset);
|
||||||
|
@ -359,7 +359,6 @@ gpgme_op_keylist_start ( GpgmeCtx c, const char *pattern, int secret_only )
|
|||||||
_gpgme_release_result (c);
|
_gpgme_release_result (c);
|
||||||
c->out_of_core = 0;
|
c->out_of_core = 0;
|
||||||
|
|
||||||
#warning This context still keeps a gpg Zombie in some cases.
|
|
||||||
if ( c->gpg ) {
|
if ( c->gpg ) {
|
||||||
_gpgme_gpg_release ( c->gpg );
|
_gpgme_gpg_release ( c->gpg );
|
||||||
c->gpg = NULL;
|
c->gpg = NULL;
|
||||||
|
191
gpgme/verify.c
191
gpgme/verify.c
@ -29,21 +29,28 @@
|
|||||||
#include "ops.h"
|
#include "ops.h"
|
||||||
|
|
||||||
struct verify_result_s {
|
struct verify_result_s {
|
||||||
|
struct verify_result_s *next;
|
||||||
GpgmeSigStat status;
|
GpgmeSigStat status;
|
||||||
GpgmeData notation; /* we store an XML fragment here */
|
GpgmeData notation; /* we store an XML fragment here */
|
||||||
|
int collecting; /* private to finish_sig() */
|
||||||
int notation_in_data; /* private to add_notation() */
|
int notation_in_data; /* private to add_notation() */
|
||||||
|
char fpr[41]; /* fingerprint of a good signature or keyid of a bad one*/
|
||||||
|
ulong timestamp; /* signature creation time */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
_gpgme_release_verify_result ( VerifyResult res )
|
_gpgme_release_verify_result ( VerifyResult res )
|
||||||
{
|
{
|
||||||
gpgme_data_release ( res->notation );
|
while (res) {
|
||||||
xfree (res);
|
VerifyResult r2 = res->next;
|
||||||
|
gpgme_data_release ( res->notation );
|
||||||
|
xfree (res);
|
||||||
|
res = r2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* fixme: check that we are adding this to the correct signature */
|
||||||
static void
|
static void
|
||||||
add_notation ( GpgmeCtx ctx, GpgStatusCode code, const char *data )
|
add_notation ( GpgmeCtx ctx, GpgStatusCode code, const char *data )
|
||||||
{
|
{
|
||||||
@ -86,9 +93,42 @@ add_notation ( GpgmeCtx ctx, GpgStatusCode code, const char *data )
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* finish a pending signature info collection and prepare for a new
|
||||||
|
* signature info collection
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
finish_sig (GpgmeCtx ctx, int stop)
|
||||||
|
{
|
||||||
|
if (stop)
|
||||||
|
return; /* nothing to do */
|
||||||
|
|
||||||
|
if (ctx->result.verify->collecting) {
|
||||||
|
VerifyResult res2;
|
||||||
|
|
||||||
|
ctx->result.verify->collecting = 0;
|
||||||
|
/* create a new result structure */
|
||||||
|
res2 = xtrycalloc ( 1, sizeof *res2 );
|
||||||
|
if ( !res2 ) {
|
||||||
|
ctx->out_of_core = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res2->next = ctx->result.verify;
|
||||||
|
ctx->result.verify = res2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->result.verify->collecting = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
verify_status_handler ( GpgmeCtx ctx, GpgStatusCode code, char *args )
|
verify_status_handler ( GpgmeCtx ctx, GpgStatusCode code, char *args )
|
||||||
{
|
{
|
||||||
|
char *p;
|
||||||
|
int i;
|
||||||
|
|
||||||
if ( ctx->out_of_core )
|
if ( ctx->out_of_core )
|
||||||
return;
|
return;
|
||||||
if ( ctx->result_type == RESULT_TYPE_NONE ) {
|
if ( ctx->result_type == RESULT_TYPE_NONE ) {
|
||||||
@ -102,20 +142,54 @@ verify_status_handler ( GpgmeCtx ctx, GpgStatusCode code, char *args )
|
|||||||
}
|
}
|
||||||
assert ( ctx->result_type == RESULT_TYPE_VERIFY );
|
assert ( ctx->result_type == RESULT_TYPE_VERIFY );
|
||||||
|
|
||||||
/* FIXME: For now we handle only one signature */
|
if (code == STATUS_GOODSIG
|
||||||
/* FIXME: Collect useful information
|
|| code == STATUS_BADSIG || code == STATUS_ERRSIG) {
|
||||||
and return them as XML */
|
finish_sig (ctx,0);
|
||||||
|
if ( ctx->out_of_core )
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case STATUS_GOODSIG:
|
case STATUS_GOODSIG:
|
||||||
ctx->result.verify->status = GPGME_SIG_STAT_GOOD;
|
/* We just look at VALIDSIG */
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case STATUS_VALIDSIG:
|
||||||
|
ctx->result.verify->status = GPGME_SIG_STAT_GOOD;
|
||||||
|
p = ctx->result.verify->fpr;
|
||||||
|
for (i=0; i < DIM(ctx->result.verify->fpr)
|
||||||
|
&& args[i] && args[i] != ' ' ; i++ )
|
||||||
|
*p++ = args[i];
|
||||||
|
*p = 0;
|
||||||
|
/* skip the formatted date */
|
||||||
|
while ( args[i] && args[i] == ' ')
|
||||||
|
i++;
|
||||||
|
while ( args[i] && args[i] != ' ')
|
||||||
|
i++;
|
||||||
|
/* and get the timestamp */
|
||||||
|
ctx->result.verify->timestamp = strtoul (args+i, NULL, 10);
|
||||||
|
break;
|
||||||
|
|
||||||
case STATUS_BADSIG:
|
case STATUS_BADSIG:
|
||||||
ctx->result.verify->status = GPGME_SIG_STAT_BAD;
|
ctx->result.verify->status = GPGME_SIG_STAT_BAD;
|
||||||
|
/* store the keyID in the fpr field */
|
||||||
|
p = ctx->result.verify->fpr;
|
||||||
|
for (i=0; i < DIM(ctx->result.verify->fpr)
|
||||||
|
&& args[i] && args[i] != ' ' ; i++ )
|
||||||
|
*p++ = args[i];
|
||||||
|
*p = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATUS_ERRSIG:
|
case STATUS_ERRSIG:
|
||||||
ctx->result.verify->status = GPGME_SIG_STAT_ERROR;
|
ctx->result.verify->status = GPGME_SIG_STAT_ERROR;
|
||||||
/* FIXME: distinguish between a regular error and a missing key.
|
/* FIXME: distinguish between a regular error and a missing key.
|
||||||
* this is encoded in the args. */
|
* this is encoded in the args. */
|
||||||
|
/* store the keyID in the fpr field */
|
||||||
|
p = ctx->result.verify->fpr;
|
||||||
|
for (i=0; i < DIM(ctx->result.verify->fpr)
|
||||||
|
&& args[i] && args[i] != ' ' ; i++ )
|
||||||
|
*p++ = args[i];
|
||||||
|
*p = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATUS_NOTATION_NAME:
|
case STATUS_NOTATION_NAME:
|
||||||
@ -127,6 +201,10 @@ verify_status_handler ( GpgmeCtx ctx, GpgStatusCode code, char *args )
|
|||||||
case STATUS_END_STREAM:
|
case STATUS_END_STREAM:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case STATUS_EOF:
|
||||||
|
finish_sig(ctx,1);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
/* ignore all other codes */
|
/* ignore all other codes */
|
||||||
break;
|
break;
|
||||||
@ -205,6 +283,21 @@ gpgme_op_verify_start ( GpgmeCtx c, GpgmeData sig, GpgmeData text )
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Figure out a common status value for all signatures
|
||||||
|
*/
|
||||||
|
static GpgmeSigStat
|
||||||
|
intersect_stati ( VerifyResult res )
|
||||||
|
{
|
||||||
|
GpgmeSigStat status = res->status;
|
||||||
|
|
||||||
|
for (res=res->next; res; res = res->next) {
|
||||||
|
if (status != res->status )
|
||||||
|
return GPGME_SIG_STAT_DIFF;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gpgme_op_verify:
|
* gpgme_op_verify:
|
||||||
* @c: the context
|
* @c: the context
|
||||||
@ -223,7 +316,8 @@ gpgme_op_verify_start ( GpgmeCtx c, GpgmeData sig, GpgmeData text )
|
|||||||
* missing key
|
* missing key
|
||||||
* GPGME_SIG_STAT_NOSIG: This is not a signature
|
* GPGME_SIG_STAT_NOSIG: This is not a signature
|
||||||
* GPGME_SIG_STAT_ERROR: Due to some other error the check could not be done.
|
* GPGME_SIG_STAT_ERROR: Due to some other error the check could not be done.
|
||||||
* FIXME: What do we return if only some o the signatures ae valid?
|
* GPGME_SIG_STAT_DIFF: There is more than 1 signature and they have not
|
||||||
|
* the same status.
|
||||||
*
|
*
|
||||||
* Return value: 0 on success or an errorcode if something not related to
|
* Return value: 0 on success or an errorcode if something not related to
|
||||||
* the signature itself did go wrong.
|
* the signature itself did go wrong.
|
||||||
@ -250,6 +344,7 @@ gpgme_op_verify ( GpgmeCtx c, GpgmeData sig, GpgmeData text,
|
|||||||
rc = mk_error (Out_Of_Core);
|
rc = mk_error (Out_Of_Core);
|
||||||
else {
|
else {
|
||||||
assert ( c->result.verify );
|
assert ( c->result.verify );
|
||||||
|
/* fixme: Put all notation data into one XML fragment */
|
||||||
if ( c->result.verify->notation ) {
|
if ( c->result.verify->notation ) {
|
||||||
GpgmeData dh = c->result.verify->notation;
|
GpgmeData dh = c->result.verify->notation;
|
||||||
|
|
||||||
@ -261,7 +356,7 @@ gpgme_op_verify ( GpgmeCtx c, GpgmeData sig, GpgmeData text,
|
|||||||
c->notation = dh;
|
c->notation = dh;
|
||||||
c->result.verify->notation = NULL;
|
c->result.verify->notation = NULL;
|
||||||
}
|
}
|
||||||
*r_stat = c->result.verify->status;
|
*r_stat = intersect_stati (c->result.verify);
|
||||||
}
|
}
|
||||||
c->pending = 0;
|
c->pending = 0;
|
||||||
}
|
}
|
||||||
@ -269,6 +364,82 @@ gpgme_op_verify ( GpgmeCtx c, GpgmeData sig, GpgmeData text,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gpgme_get_sig_status:
|
||||||
|
* @c: Context
|
||||||
|
* @idx: Index of the signature starting at 0
|
||||||
|
* @r_stat: Returns the status
|
||||||
|
* @r_created: Returns the creation timestamp
|
||||||
|
*
|
||||||
|
* Return information about an already verified signatures.
|
||||||
|
*
|
||||||
|
* Return value: The fingerprint or NULL in case of an problem or
|
||||||
|
* when there are no more signatures.
|
||||||
|
**/
|
||||||
|
const char *
|
||||||
|
gpgme_get_sig_status (GpgmeCtx c, int idx,
|
||||||
|
GpgmeSigStat *r_stat, time_t *r_created )
|
||||||
|
{
|
||||||
|
VerifyResult res;
|
||||||
|
|
||||||
|
if (!c || c->pending || c->result_type != RESULT_TYPE_VERIFY )
|
||||||
|
return NULL; /* No results yet or verification error */
|
||||||
|
|
||||||
|
for (res = c->result.verify; res && idx>0 ; res = res->next, idx--)
|
||||||
|
;
|
||||||
|
if (!res)
|
||||||
|
return NULL; /* No more signatures */
|
||||||
|
|
||||||
|
if (r_stat)
|
||||||
|
*r_stat = res->status;
|
||||||
|
if (r_created)
|
||||||
|
*r_created = res->timestamp;
|
||||||
|
return res->fpr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gpgme_get_sig_key:
|
||||||
|
* @c: context
|
||||||
|
* @idx: Index of the signature starting at 0
|
||||||
|
* @r_key: Returns the key object
|
||||||
|
*
|
||||||
|
* Return a key object which was used to check the signature.
|
||||||
|
*
|
||||||
|
* Return value: An Errorcode or 0 for success. GPG<ME_EOF is returned to
|
||||||
|
* indicate that there are no more signatures.
|
||||||
|
**/
|
||||||
|
GpgmeError
|
||||||
|
gpgme_get_sig_key (GpgmeCtx c, int idx, GpgmeKey *r_key)
|
||||||
|
{
|
||||||
|
VerifyResult res;
|
||||||
|
GpgmeCtx listctx;
|
||||||
|
GpgmeError err;
|
||||||
|
|
||||||
|
if (!c || !r_key)
|
||||||
|
return mk_error (Invalid_Value);
|
||||||
|
if (c->pending || c->result_type != RESULT_TYPE_VERIFY )
|
||||||
|
return mk_error (Busy);
|
||||||
|
|
||||||
|
for (res = c->result.verify; res && idx>0 ; res = res->next, idx--)
|
||||||
|
;
|
||||||
|
if (!res)
|
||||||
|
return mk_error (EOF);
|
||||||
|
|
||||||
|
if (strlen(res->fpr) < 16) /* we have at least an key ID */
|
||||||
|
return mk_error (Invalid_Key);
|
||||||
|
|
||||||
|
/* Fixme: This can me optimized keeping
|
||||||
|
* an internal context used for such key listings */
|
||||||
|
if ( (err=gpgme_new (&listctx)) )
|
||||||
|
return err;
|
||||||
|
if ( !(err=gpgme_op_keylist_start (listctx, res->fpr, 0 )) )
|
||||||
|
err=gpgme_op_keylist_next ( listctx, r_key );
|
||||||
|
gpgme_release (listctx);
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,28 +67,59 @@ static const char test_sig1[] =
|
|||||||
exit (1); } \
|
exit (1); } \
|
||||||
} while(0)
|
} while(0)
|
||||||
|
|
||||||
static void
|
|
||||||
print_sig_stat ( GpgmeSigStat status )
|
static const char *
|
||||||
|
status_string (GpgmeSigStat status)
|
||||||
{
|
{
|
||||||
|
const char *s = "?";
|
||||||
|
|
||||||
switch ( status ) {
|
switch ( status ) {
|
||||||
case GPGME_SIG_STAT_NONE:
|
case GPGME_SIG_STAT_NONE:
|
||||||
fputs ("Verification Status: None\n", stdout);
|
s = "None";
|
||||||
break;
|
break;
|
||||||
case GPGME_SIG_STAT_NOSIG:
|
case GPGME_SIG_STAT_NOSIG:
|
||||||
fputs ("Verification Status: No Signature\n", stdout);
|
s = "No Signature";
|
||||||
break;
|
break;
|
||||||
case GPGME_SIG_STAT_GOOD:
|
case GPGME_SIG_STAT_GOOD:
|
||||||
fputs ("Verification Status: Good\n", stdout);
|
s = "Good";
|
||||||
break;
|
break;
|
||||||
case GPGME_SIG_STAT_BAD:
|
case GPGME_SIG_STAT_BAD:
|
||||||
fputs ("Verification Status: Bad\n", stdout);
|
s = "Bad";
|
||||||
break;
|
break;
|
||||||
case GPGME_SIG_STAT_NOKEY:
|
case GPGME_SIG_STAT_NOKEY:
|
||||||
fputs ("Verification Status: No Key\n", stdout);
|
s = "No Key";
|
||||||
break;
|
break;
|
||||||
case GPGME_SIG_STAT_ERROR:
|
case GPGME_SIG_STAT_ERROR:
|
||||||
fputs ("Verification Status: Error\n", stdout);
|
s = "Error";
|
||||||
break;
|
break;
|
||||||
|
case GPGME_SIG_STAT_DIFF:
|
||||||
|
s = "More than one signature";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
print_sig_stat ( GpgmeCtx ctx, GpgmeSigStat status )
|
||||||
|
{
|
||||||
|
const char *s;
|
||||||
|
time_t created;
|
||||||
|
int idx;
|
||||||
|
GpgmeKey key;
|
||||||
|
|
||||||
|
printf ("Verification Status: %s\n", status_string (status));
|
||||||
|
|
||||||
|
for(idx=0; (s=gpgme_get_sig_status (ctx, idx, &status, &created)); idx++ ) {
|
||||||
|
printf ("sig %d: created: %lu status: %s\n", idx, (ulong)created,
|
||||||
|
status_string(status) );
|
||||||
|
printf ("sig %d: fpr/keyid=`%s'\n", idx, s );
|
||||||
|
if ( !gpgme_get_sig_key (ctx, idx, &key) ) {
|
||||||
|
char *p = gpgme_key_get_as_xml ( key );
|
||||||
|
printf ("sig %d: key object:\n%s\n", idx, p );
|
||||||
|
free (p);
|
||||||
|
gpgme_key_release (key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +149,7 @@ main (int argc, char **argv )
|
|||||||
|
|
||||||
puts ("checking a valid message:\n");
|
puts ("checking a valid message:\n");
|
||||||
err = gpgme_op_verify (ctx, sig, text, &status );
|
err = gpgme_op_verify (ctx, sig, text, &status );
|
||||||
print_sig_stat ( status );
|
print_sig_stat ( ctx, status );
|
||||||
fail_if_err (err);
|
fail_if_err (err);
|
||||||
|
|
||||||
if ( (nota=gpgme_get_notation (ctx)) )
|
if ( (nota=gpgme_get_notation (ctx)) )
|
||||||
@ -131,7 +162,7 @@ main (int argc, char **argv )
|
|||||||
fail_if_err (err);
|
fail_if_err (err);
|
||||||
gpgme_data_rewind ( sig );
|
gpgme_data_rewind ( sig );
|
||||||
err = gpgme_op_verify (ctx, sig, text, &status );
|
err = gpgme_op_verify (ctx, sig, text, &status );
|
||||||
print_sig_stat ( status );
|
print_sig_stat ( ctx, status );
|
||||||
fail_if_err (err);
|
fail_if_err (err);
|
||||||
if ( (nota=gpgme_get_notation (ctx)) )
|
if ( (nota=gpgme_get_notation (ctx)) )
|
||||||
printf ("---Begin Notation---\n%s---End Notation---\n", nota );
|
printf ("---Begin Notation---\n%s---End Notation---\n", nota );
|
||||||
|
Loading…
Reference in New Issue
Block a user