Generally working.
This commit is contained in:
@@ -14,3 +14,4 @@ This file provides guidance to agents when working with code in this repository.
|
|||||||
- **Development Ports**: Local development uses port 9001, production typically uses nginx proxy on standard ports
|
- **Development Ports**: Local development uses port 9001, production typically uses nginx proxy on standard ports
|
||||||
- **Setup Wizard**: Interactive setup creates cryptographically signed config files - not typical config generation
|
- **Setup Wizard**: Interactive setup creates cryptographically signed config files - not typical config generation
|
||||||
- **Extension Handling**: nginx config uses wildcards to serve files regardless of URL extension - Blossom protocol compliance
|
- **Extension Handling**: nginx config uses wildcards to serve files regardless of URL extension - Blossom protocol compliance
|
||||||
|
- **Configuration Events**: Uses Nostr kind 33333 events with 1-year default expiration - XDG paths take priority over database
|
||||||
@@ -16,3 +16,7 @@ This file provides guidance to agents when working with code in this repository.
|
|||||||
- **Blob Metadata**: Database is single source of truth - use `get_blob_metadata()`, not filesystem checks
|
- **Blob Metadata**: Database is single source of truth - use `get_blob_metadata()`, not filesystem checks
|
||||||
- **nostr_core_lib Build**: Uses `build.sh` script, NOT `make` - run `./build.sh` to compile the library
|
- **nostr_core_lib Build**: Uses `build.sh` script, NOT `make` - run `./build.sh` to compile the library
|
||||||
- **Server Testing**: Use `./restart-all.sh` to properly restart and test ginxsom server, NOT direct binary execution
|
- **Server Testing**: Use `./restart-all.sh` to properly restart and test ginxsom server, NOT direct binary execution
|
||||||
|
- **spawn-fcgi Parameters**: Must use `-M 666 -u "$USER" -g "$USER"` for socket permissions and ownership
|
||||||
|
- **Admin Event Structure**: Admin auth uses kind 24242 with "t" tag containing HTTP method, expiration typically 1 hour
|
||||||
|
- **Key File Permissions**: `.admin_keys` file must be chmod 600 or auth tests will fail
|
||||||
|
- **Development Environment**: Set `GINX_DEBUG=1` for pubkey extraction diagnostics during development
|
||||||
@@ -14,3 +14,5 @@ This file provides guidance to agents when working with code in this repository.
|
|||||||
- **Admin Key Mismatch**: Database admin_pubkey vs .admin_keys file often cause auth failures
|
- **Admin Key Mismatch**: Database admin_pubkey vs .admin_keys file often cause auth failures
|
||||||
- **Nginx Port Conflicts**: Local nginx on 9001 conflicts with system nginx on 80 - check with `netstat -tlnp`
|
- **Nginx Port Conflicts**: Local nginx on 9001 conflicts with system nginx on 80 - check with `netstat -tlnp`
|
||||||
- **Hash Calculation**: File data buffer must be complete before `nostr_sha256()` call or hash is wrong
|
- **Hash Calculation**: File data buffer must be complete before `nostr_sha256()` call or hash is wrong
|
||||||
|
- **Admin Key File**: `.admin_keys` must be chmod 600 and sourceable by bash - common test failure cause
|
||||||
|
- **Process Cleanup**: `restart-all.sh` performs force kill if graceful shutdown fails - check logs for hung processes
|
||||||
BIN
build/bud06.o
BIN
build/bud06.o
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
db/ginxsom.db
BIN
db/ginxsom.db
Binary file not shown.
21
src/bud06.c
21
src/bud06.c
@@ -204,14 +204,7 @@ void handle_head_upload_request(void) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if blob already exists (duplicate detection)
|
// Check for authorization first (before duplicate detection)
|
||||||
if (check_blob_exists(sha256)) {
|
|
||||||
send_upload_error_response(409, "blob_exists", "Blob with this hash already exists", XREASON_BLOB_EXISTS);
|
|
||||||
log_request("HEAD", "/upload", "none", 409);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for optional authorization
|
|
||||||
const char* auth_header = getenv("HTTP_AUTHORIZATION");
|
const char* auth_header = getenv("HTTP_AUTHORIZATION");
|
||||||
const char* auth_status = "none";
|
const char* auth_status = "none";
|
||||||
|
|
||||||
@@ -220,6 +213,18 @@ void handle_head_upload_request(void) {
|
|||||||
// This handler receives pre-validated requests, so if we reach here with auth_header,
|
// This handler receives pre-validated requests, so if we reach here with auth_header,
|
||||||
// the authentication was already successful
|
// the authentication was already successful
|
||||||
auth_status = "authenticated";
|
auth_status = "authenticated";
|
||||||
|
} else {
|
||||||
|
// Check if server requires authorization for uploads
|
||||||
|
// If auth is required but not provided, return 401 before checking for duplicates
|
||||||
|
// TODO: This should check server configuration for auth requirements
|
||||||
|
// For now, assume auth is optional unless configured otherwise
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if blob already exists (duplicate detection - after auth validation)
|
||||||
|
if (check_blob_exists(sha256)) {
|
||||||
|
send_upload_error_response(409, "blob_exists", "Blob with this hash already exists", XREASON_BLOB_EXISTS);
|
||||||
|
log_request("HEAD", "/upload", auth_status, 409);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All validations passed - return success
|
// All validations passed - return success
|
||||||
|
|||||||
105
src/main.c
105
src/main.c
@@ -28,16 +28,6 @@
|
|||||||
// Database path
|
// Database path
|
||||||
#define DB_PATH "db/ginxsom.db"
|
#define DB_PATH "db/ginxsom.db"
|
||||||
|
|
||||||
// ===== COMMENTED OUT UNUSED CODE =====
|
|
||||||
// Forward declarations for config system (all commented out)
|
|
||||||
/*
|
|
||||||
int initialize_server_config(void);
|
|
||||||
int apply_config_from_event(cJSON *event);
|
|
||||||
int get_config_file_path(char *path, size_t path_size);
|
|
||||||
int load_server_config(const char *config_path);
|
|
||||||
int run_interactive_setup(const char *config_path);
|
|
||||||
*/
|
|
||||||
// ===== END COMMENTED OUT CODE =====
|
|
||||||
|
|
||||||
// Configuration system implementation
|
// Configuration system implementation
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -536,15 +526,6 @@ const char *extract_sha256_from_uri(const char *uri) {
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// ===== COMMENTED OUT UNUSED CODE =====
|
|
||||||
// Forward declarations for detailed validation functions (all commented out)
|
|
||||||
/*
|
|
||||||
int detailed_structure_validation(cJSON *event);
|
|
||||||
int detailed_signature_validation(cJSON *event);
|
|
||||||
void analyze_event_fields(cJSON *event);
|
|
||||||
void hex_dump(const char *label, const unsigned char *data, size_t len);
|
|
||||||
*/
|
|
||||||
// ===== END COMMENTED OUT CODE =====
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1024,38 +1005,7 @@ void handle_upload_request(void) {
|
|||||||
|
|
||||||
fflush(stderr);
|
fflush(stderr);
|
||||||
|
|
||||||
// Legacy authentication check - now handled by centralized validation system
|
|
||||||
/*
|
|
||||||
// Check if authentication rules are enabled using nostr_core_lib system
|
|
||||||
int auth_required = nostr_auth_rules_enabled();
|
|
||||||
fprintf(stderr, "AUTH: auth_rules_enabled = %d, auth_header present: %s\r\n",
|
|
||||||
auth_required, auth_header ? "YES" : "NO");
|
|
||||||
|
|
||||||
// If authentication is required but no auth header provided, fail immediately
|
|
||||||
if (auth_required && !auth_header) {
|
|
||||||
free(file_data);
|
|
||||||
send_error_response(401, "authorization_required",
|
|
||||||
"Authorization required for upload operations",
|
|
||||||
"This server requires authentication for all uploads");
|
|
||||||
log_request("PUT", "/upload", "missing_auth", 401);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If auth rules are completely disabled, skip all validation and allow upload
|
|
||||||
if (!auth_required) {
|
|
||||||
fprintf(stderr, "AUTH: Authentication rules disabled - skipping all "
|
|
||||||
"validation and allowing upload\n");
|
|
||||||
// Skip validation and proceed to file processing
|
|
||||||
goto process_file_upload;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Authentication is handled by centralized validation system
|
|
||||||
// TODO: Get uploader_pubkey from centralized validation result
|
|
||||||
// For now, keep existing uploader_pubkey extraction for compatibility
|
|
||||||
|
|
||||||
// Legacy goto label - no longer needed with centralized validation
|
|
||||||
// process_file_upload:
|
|
||||||
// Get dimensions from in-memory buffer before saving file
|
// Get dimensions from in-memory buffer before saving file
|
||||||
int width = 0, height = 0;
|
int width = 0, height = 0;
|
||||||
nip94_get_dimensions(file_data, content_length, content_type, &width,
|
nip94_get_dimensions(file_data, content_length, content_type, &width,
|
||||||
@@ -1299,37 +1249,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re
|
|||||||
|
|
||||||
fflush(stderr);
|
fflush(stderr);
|
||||||
|
|
||||||
// Legacy authentication check - now handled by centralized validation system
|
|
||||||
/*
|
|
||||||
// Check if authentication rules are enabled using nostr_core_lib system
|
|
||||||
int auth_required = nostr_auth_rules_enabled();
|
|
||||||
fprintf(stderr, "AUTH: auth_rules_enabled = %d, auth_header present: %s\r\n",
|
|
||||||
auth_required, auth_header ? "YES" : "NO");
|
|
||||||
|
|
||||||
// If authentication is required but no auth header provided, fail immediately
|
|
||||||
if (auth_required && !auth_header) {
|
|
||||||
if (should_free_file_data) free(file_data);
|
|
||||||
send_error_response(401, "authorization_required",
|
|
||||||
"Authorization required for upload operations",
|
|
||||||
"This server requires authentication for all uploads");
|
|
||||||
log_request("PUT", "/upload", "missing_auth", 401);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If auth rules are completely disabled, skip all validation and allow upload
|
|
||||||
if (!auth_required) {
|
|
||||||
fprintf(stderr, "AUTH: Authentication rules disabled - skipping all "
|
|
||||||
"validation and allowing upload\n");
|
|
||||||
// Skip validation and proceed to file processing
|
|
||||||
goto process_file_upload;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Authentication was handled by centralized validation system
|
|
||||||
// uploader_pubkey should be set from validation result
|
|
||||||
|
|
||||||
// Legacy goto label - no longer needed with centralized validation
|
|
||||||
// process_file_upload:
|
|
||||||
// Get dimensions from in-memory buffer before saving file
|
// Get dimensions from in-memory buffer before saving file
|
||||||
int width = 0, height = 0;
|
int width = 0, height = 0;
|
||||||
nip94_get_dimensions(file_data, file_size, content_type, &width,
|
nip94_get_dimensions(file_data, file_size, content_type, &width,
|
||||||
@@ -1523,23 +1443,6 @@ int main(void) {
|
|||||||
// Try file-based config first, then fall back to database config
|
// Try file-based config first, then fall back to database config
|
||||||
int config_loaded = 0;
|
int config_loaded = 0;
|
||||||
|
|
||||||
// All file-based config and interactive setup are commented out
|
|
||||||
/*
|
|
||||||
char config_path[512];
|
|
||||||
|
|
||||||
if (get_config_file_path(config_path, sizeof(config_path))) {
|
|
||||||
fprintf(stderr, "STARTUP: Checking for config file at: %s\n", config_path);
|
|
||||||
if (load_server_config(config_path)) {
|
|
||||||
fprintf(stderr,
|
|
||||||
"STARTUP: File-based configuration loaded successfully\n");
|
|
||||||
config_loaded = 1;
|
|
||||||
} else {
|
|
||||||
fprintf(stderr, "STARTUP: No valid file-based config found, trying "
|
|
||||||
"database config\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Fall back to database configuration if file config failed
|
// Fall back to database configuration if file config failed
|
||||||
if (!config_loaded /* && !initialize_server_config() */) {
|
if (!config_loaded /* && !initialize_server_config() */) {
|
||||||
fprintf(
|
fprintf(
|
||||||
@@ -1547,13 +1450,7 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
|||||||
"STARTUP: No configuration found - server starting in setup mode\n");
|
"STARTUP: No configuration found - server starting in setup mode\n");
|
||||||
fprintf(stderr, "STARTUP: Run interactive setup with: ginxsom --setup\n");
|
fprintf(stderr, "STARTUP: Run interactive setup with: ginxsom --setup\n");
|
||||||
// For interactive mode (when stdin is available), offer setup
|
// For interactive mode (when stdin is available), offer setup
|
||||||
/*
|
|
||||||
char config_path[512];
|
|
||||||
if (isatty(STDIN_FILENO) &&
|
|
||||||
get_config_file_path(config_path, sizeof(config_path))) {
|
|
||||||
return run_interactive_setup(config_path);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
} else if (!config_loaded) {
|
} else if (!config_loaded) {
|
||||||
fprintf(stderr, "STARTUP: Database configuration loaded successfully\n");
|
fprintf(stderr, "STARTUP: Database configuration loaded successfully\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,7 +283,17 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
|
|||||||
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
|
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Check if authentication header is provided
|
// Check if this is a BUD-09 report request - allow anonymous reporting
|
||||||
|
if (request->operation && strcmp(request->operation, "report") == 0) {
|
||||||
|
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation
|
||||||
|
result->valid = 1;
|
||||||
|
result->error_code = NOSTR_SUCCESS;
|
||||||
|
strcpy(result->reason, "BUD-09 report request - bypassing auth for anonymous reporting");
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: BUD-09 report detected, bypassing authentication\n");
|
||||||
|
return NOSTR_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if authentication header is provided (required for non-report operations)
|
||||||
if (!request->auth_header) {
|
if (!request->auth_header) {
|
||||||
|
|
||||||
result->valid = 0;
|
result->valid = 0;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
09127399ac6d531773cafe433bd6ffd0592b04480543b8225ba17d48fd61b5ac
|
e3ba927d32ca105a8a4cafa2e013b97945a165c38e9ce573446a2332dc312fdb
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Admin Initialization Script for Ginxsom Testing
|
|
||||||
# Sets up the test admin key in the database
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Test admin public key (must match TEST_ADMIN_PUBKEY from admin_test.sh)
|
|
||||||
TEST_ADMIN_PUBKEY="2ef05348f28d24e0f0ed0751278442c27b62c823c37af8d8d89d8592c6ee84e7"
|
|
||||||
|
|
||||||
echo "Initializing admin access for testing..."
|
|
||||||
|
|
||||||
# Check if database exists
|
|
||||||
if [ ! -f "db/ginxsom.db" ]; then
|
|
||||||
echo "Error: Database db/ginxsom.db not found. Run ./db/init.sh first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Configure admin settings
|
|
||||||
sqlite3 db/ginxsom.db << EOF
|
|
||||||
INSERT OR REPLACE INTO config (key, value, description) VALUES
|
|
||||||
('admin_pubkey', '$TEST_ADMIN_PUBKEY', 'Nostr public key authorized for admin operations (test key)'),
|
|
||||||
('admin_enabled', 'true', 'Enable admin interface');
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Admin access configured successfully!"
|
|
||||||
echo "Test admin public key: $TEST_ADMIN_PUBKEY"
|
|
||||||
echo "Use private key from admin_test.sh to generate authentication tokens"
|
|
||||||
|
|
||||||
# Verify configuration
|
|
||||||
echo ""
|
|
||||||
echo "Current admin configuration:"
|
|
||||||
sqlite3 db/ginxsom.db "SELECT key, value FROM config WHERE key IN ('admin_pubkey', 'admin_enabled');"
|
|
||||||
@@ -11,11 +11,16 @@ echo "=== BUD-06 Upload Requirements Test Suite ==="
|
|||||||
SERVER_URL="http://localhost:9001"
|
SERVER_URL="http://localhost:9001"
|
||||||
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
|
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
|
||||||
|
|
||||||
# Test file properties
|
# Test file properties - generate unique hashes for each test run
|
||||||
TEST_SHA256="24308d48eb498b593e55a87b6300ccffdea8432babc0bb898b1eff21ebbb72de"
|
TIMESTAMP=$(date +%s)
|
||||||
|
TEST_SHA256=$(echo "test_bud06_${TIMESTAMP}_$(uname -n)" | sha256sum | cut -d' ' -f1)
|
||||||
TEST_CONTENT_TYPE="image/png"
|
TEST_CONTENT_TYPE="image/png"
|
||||||
TEST_CONTENT_LENGTH="71418"
|
TEST_CONTENT_LENGTH="71418"
|
||||||
|
|
||||||
|
# Generate additional unique hashes for tests that need fresh hashes
|
||||||
|
TEST_SHA256_FRESH=$(echo "fresh_bud06_${TIMESTAMP}_$(uname -n)" | sha256sum | cut -d' ' -f1)
|
||||||
|
TEST_SHA256_AUTH=$(echo "auth_bud06_${TIMESTAMP}_$(uname -n)" | sha256sum | cut -d' ' -f1)
|
||||||
|
|
||||||
# Helper function to make HEAD request with custom headers
|
# Helper function to make HEAD request with custom headers
|
||||||
make_head_request() {
|
make_head_request() {
|
||||||
local sha256="$1"
|
local sha256="$1"
|
||||||
@@ -59,11 +64,11 @@ echo ""
|
|||||||
echo "=== Test 1: Valid Upload Requirements ==="
|
echo "=== Test 1: Valid Upload Requirements ==="
|
||||||
echo "Testing HEAD /upload with valid headers..."
|
echo "Testing HEAD /upload with valid headers..."
|
||||||
|
|
||||||
RESPONSE=$(make_head_request "$TEST_SHA256" "$TEST_CONTENT_TYPE" "$TEST_CONTENT_LENGTH")
|
RESPONSE=$(make_head_request "$TEST_SHA256_FRESH" "$TEST_CONTENT_TYPE" "$TEST_CONTENT_LENGTH")
|
||||||
STATUS=$(get_status_code "$RESPONSE")
|
STATUS=$(get_status_code "$RESPONSE")
|
||||||
|
|
||||||
echo "Request Headers:"
|
echo "Request Headers:"
|
||||||
echo " X-SHA-256: $TEST_SHA256"
|
echo " X-SHA-256: $TEST_SHA256_FRESH"
|
||||||
echo " X-Content-Type: $TEST_CONTENT_TYPE"
|
echo " X-Content-Type: $TEST_CONTENT_TYPE"
|
||||||
echo " X-Content-Length: $TEST_CONTENT_LENGTH"
|
echo " X-Content-Length: $TEST_CONTENT_LENGTH"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -185,12 +190,12 @@ echo "=== Test 6: Unsupported Media Type ==="
|
|||||||
echo "Testing HEAD /upload with potentially unsupported MIME type..."
|
echo "Testing HEAD /upload with potentially unsupported MIME type..."
|
||||||
|
|
||||||
UNSUPPORTED_TYPE="application/x-malware"
|
UNSUPPORTED_TYPE="application/x-malware"
|
||||||
RESPONSE=$(make_head_request "$TEST_SHA256" "$UNSUPPORTED_TYPE" "$TEST_CONTENT_LENGTH")
|
RESPONSE=$(make_head_request "$TEST_SHA256_FRESH" "$UNSUPPORTED_TYPE" "$TEST_CONTENT_LENGTH")
|
||||||
STATUS=$(get_status_code "$RESPONSE")
|
STATUS=$(get_status_code "$RESPONSE")
|
||||||
REASON=$(get_x_reason "$RESPONSE")
|
REASON=$(get_x_reason "$RESPONSE")
|
||||||
|
|
||||||
echo "Request Headers:"
|
echo "Request Headers:"
|
||||||
echo " X-SHA-256: $TEST_SHA256"
|
echo " X-SHA-256: $TEST_SHA256_FRESH"
|
||||||
echo " X-Content-Type: $UNSUPPORTED_TYPE"
|
echo " X-Content-Type: $UNSUPPORTED_TYPE"
|
||||||
echo " X-Content-Length: $TEST_CONTENT_LENGTH"
|
echo " X-Content-Length: $TEST_CONTENT_LENGTH"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -267,12 +272,12 @@ echo "=== Test 9: Authorization Handling ==="
|
|||||||
echo "Testing HEAD /upload authorization requirements..."
|
echo "Testing HEAD /upload authorization requirements..."
|
||||||
|
|
||||||
# Test without authorization first
|
# Test without authorization first
|
||||||
RESPONSE=$(make_head_request "$TEST_SHA256" "$TEST_CONTENT_TYPE" "$TEST_CONTENT_LENGTH")
|
RESPONSE=$(make_head_request "$TEST_SHA256_AUTH" "$TEST_CONTENT_TYPE" "$TEST_CONTENT_LENGTH")
|
||||||
STATUS=$(get_status_code "$RESPONSE")
|
STATUS=$(get_status_code "$RESPONSE")
|
||||||
REASON=$(get_x_reason "$RESPONSE")
|
REASON=$(get_x_reason "$RESPONSE")
|
||||||
|
|
||||||
echo "Request Headers (no authorization):"
|
echo "Request Headers (no authorization):"
|
||||||
echo " X-SHA-256: $TEST_SHA256"
|
echo " X-SHA-256: $TEST_SHA256_AUTH"
|
||||||
echo " X-Content-Type: $TEST_CONTENT_TYPE"
|
echo " X-Content-Type: $TEST_CONTENT_TYPE"
|
||||||
echo " X-Content-Length: $TEST_CONTENT_LENGTH"
|
echo " X-Content-Length: $TEST_CONTENT_LENGTH"
|
||||||
echo " Authorization: (missing)"
|
echo " Authorization: (missing)"
|
||||||
|
|||||||
Reference in New Issue
Block a user