Initial commit.

This commit is contained in:
Artyom Titov 2026-05-15 18:00:40 +03:00
commit 24da35f11e
9 changed files with 1753 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
uploads/
server.log
ssl-dir/*.key

1
activate Symbolic link
View file

@ -0,0 +1 @@
/home/archibaldik/progs/nas-python/.venv/naspy/bin/activate

198
home.html Normal file
View file

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Network File Transfer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
h1 { color: #333; margin-bottom: 10px; text-align: center; }
.subtitle { text-align: center; color: #666; margin-bottom: 30px; font-size: 14px; }
.section { margin-bottom: 30px; }
.section h2 { font-size: 16px; color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 8px; }
.upload-area {
border: 2px dashed #667eea;
border-radius: 8px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: #f8f9ff;
}
.upload-area:hover { background: #f0f2ff; border-color: #764ba2; }
.upload-area.dragover { background: #e8ebff; border-color: #764ba2; }
#fileInput { display: none; }
.btn {
display: inline-block;
background: #667eea;
color: white;
padding: 10px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.3s;
}
.btn:hover { background: #764ba2; }
.btn-secondary { background: #6c757d; }
.btn-secondary:hover { background: #5a6268; }
.file-list {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
max-height: 300px;
overflow-y: auto;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: white;
border-radius: 6px;
margin-bottom: 8px;
border-left: 4px solid #667eea;
}
.file-name { flex: 1; color: #333; word-break: break-all; font-size: 14px; }
.file-actions { display: flex; gap: 8px; margin-left: 10px; }
.btn-small { padding: 6px 12px; font-size: 12px; }
.message {
padding: 12px;
border-radius: 6px;
margin-bottom: 15px;
display: none;
font-size: 14px;
}
.message.show { display: block; }
.message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.no-files { text-align: center; color: #999; padding: 20px; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>📁 Network File Transfer</h1>
<p class="subtitle">Upload and download files from your network drive</p>
<div id="message" class="message"></div>
<div class="section">
<h2>📤 Upload Files</h2>
<div class="upload-area" id="uploadArea">
<p>📁 Drag and drop files here</p>
<p style="font-size: 12px; color: #999;">or click to browse</p>
<button class="btn btn-secondary btn-small">Choose Files</button>
</div>
<input type="file" id="fileInput" multiple>
</div>
<div class="section">
<h2>📥 Download Files</h2>
<div class="file-list" id="fileList">
<div class="no-files">Loading files...</div>
</div>
</div>
</div>
<script>
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const message = document.getElementById('message');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
uploadFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => uploadFiles(e.target.files));
async function uploadFiles(files) {
for (let file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/upload.sh', { method: 'POST', body: formData });
if (response.ok) {
showMessage(`✓ ${file.name} uploaded`, 'success');
loadFileList();
} else {
showMessage(`✗ Error uploading ${file.name}`, 'error');
}
} catch (error) {
showMessage(`✗ Error: ${error.message}`, 'error');
}
}
fileInput.value = '';
}
async function loadFileList() {
try {
const response = await fetch('/list.sh');
const text = await response.text();
const files = text.trim().split('\n').filter(f => f);
fileList.innerHTML = files.length === 0
? '<div class="no-files">No files yet</div>'
: files.map(file => `
<div class="file-item">
<span class="file-name">${file}</span>
<div class="file-actions">
<a href="/download.sh?file=${encodeURIComponent(file)}" class="btn btn-small">Download</a>
<button class="btn btn-small" style="background: #dc3545;" onclick="deleteFile('${file}')">Delete</button>
</div>
</div>
`).join('');
} catch (error) {
fileList.innerHTML = '<div class="no-files">Error loading files</div>';
}
}
async function deleteFile(filename) {
if (!confirm(`Delete ${filename}?`)) return;
try {
const response = await fetch(`/delete.sh?file=${encodeURIComponent(filename)}`, { method: 'POST' });
if (response.ok) {
showMessage(`✓ ${filename} deleted`, 'success');
loadFileList();
} else {
showMessage(`✗ Error deleting ${filename}`, 'error');
}
} catch (error) {
showMessage(`✗ Error: ${error.message}`, 'error');
}
}
function showMessage(text, type) {
message.textContent = text;
message.className = `message show ${type}`;
setTimeout(() => message.classList.remove('show'), 4000);
}
loadFileList();
setInterval(loadFileList, 5000);
</script>
</body>
</html>

1041
index.html Normal file

File diff suppressed because it is too large Load diff

39
server.py Normal file
View file

@ -0,0 +1,39 @@
import ssl
from http.server import BaseHTTPRequestHandler, HTTPServer
import socketserver
with open("home.html", "r", encoding="utf-8" ) as f:
greeting_page = f.read()
class Handler(BaseHTTPRequestHandler):
def sql_req(self):
data = ""
d_type = ""
if self.path == "/":
d_type = "text/html"
data = greeting_page
return (data, d_type)
def do_GET(self):
if self.path != "/":
self.send_response(404)
self.end_headers()
return
self.send_response(200)
self.send_header("Data type", self.sql_req()[1])
self.end_headers()
self.wfile.write(self.sql_req()[0].encode())
server = HTTPServer(("0.0.0.0", 4443), Handler)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(certfile="ssl-dir/server.crt",keyfile="ssl-dir/server.key")
httpd = socketserver.TCPServer(("0.0.0.0", 4445), Handler)
httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side = True)
server.socket = ssl_context.wrap_socket(server.socket, server_side=True)
server.serve_forever()

384
server_core.py Normal file
View file

@ -0,0 +1,384 @@
#!/usr/bin/env python3
"""
HTTPS File Transfer Server for OpenWRT/Notebook
Secure file upload, download, and management over HTTPS
"""
import ssl
import os
import json
import mimetypes
import urllib.parse
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime
import logging
# ============================================================================
# Configuration
# ============================================================================
# Server settings
HOST = "0.0.0.0"
HTTP_PORT = 8080
HTTPS_PORT = 4443
# File settings
UPLOAD_DIR = "./uploads"
CERT_DIR = "./ssl-dir"
SERVER_CERT = os.path.join(CERT_DIR, "server.crt")
SERVER_KEY = os.path.join(CERT_DIR, "server.key")
# Logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('server.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# ============================================================================
# Utility Functions
# ============================================================================
def ensure_upload_dir():
"""Create upload directory if it doesn't exist"""
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
logger.info(f"Upload directory: {os.path.abspath(UPLOAD_DIR)}")
def ensure_ssl_certs():
"""Check if SSL certificates exist"""
if not os.path.exists(SERVER_CERT) or not os.path.exists(SERVER_KEY):
logger.error(f"SSL certificates not found in {CERT_DIR}")
logger.info("Generate certificates with:")
logger.info(f" mkdir -p {CERT_DIR}")
logger.info(f" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \\")
logger.info(f" -keyout {SERVER_KEY} -out {SERVER_CERT} \\")
logger.info(f" -subj '/C=US/ST=State/L=City/O=Org/CN=localhost'")
return False
return True
def get_file_list():
"""Get list of files in upload directory
Skips directories"""
try:
files = []
for filename in os.listdir(UPLOAD_DIR):
filepath = os.path.join(UPLOAD_DIR, filename)
if os.path.isfile(filepath):
size = os.path.getsize(filepath)
mtime = os.path.getmtime(filepath)
files.append({
"name": filename,
"size": size,
"modified": datetime.fromtimestamp(mtime).isoformat(),
"size_human": format_size(size)
})
return sorted(files, key=lambda x: x['modified'], reverse=True)
except Exception as e:
logger.error(f"Error listing files: {e}")
return []
def format_size(bytes_size):
"""Convert bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes_size < 1024:
return f"{bytes_size:.1f}{unit}"
bytes_size /= 1024
return f"{bytes_size:.1f}TB"
def get_content_type(filename):
"""Get MIME type for file"""
mime_type, _ = mimetypes.guess_type(filename)
return mime_type or 'application/octet-stream'
# ============================================================================
# HTML/CSS/JavaScript
# ============================================================================
with open("index.html", "r", encoding="utf-8") as f:
HTML_TEMPLATE = f.read()
# ============================================================================
# Request Handler
# ============================================================================
class FileTransferHandler(BaseHTTPRequestHandler):
"""HTTP request handler for file transfer operations"""
def log_message(self, format, *args):
"""Override logging to use our logger"""
logger.info(f"{self.client_address[0]} - {format % args}")
def do_GET(self):
"""Handle GET requests"""
try:
# Parse URL
parsed_url = urllib.parse.urlparse(self.path)
path = parsed_url.path
query_params = urllib.parse.parse_qs(parsed_url.query)
# Route handling
if path == "/":
self.serve_index()
elif path == "/api/files":
self.serve_file_list()
elif path == "/download":
filename = query_params.get('file', [None])[0]
if filename:
self.serve_download(filename)
else:
self.send_error(400, "Missing file parameter")
elif path == "/status":
self.serve_status()
else:
self.send_error(404, "Not found")
except Exception as e:
logger.error(f"GET error: {e}")
self.send_error(500, "Internal server error")
def do_POST(self):
"""Handle POST requests"""
try:
parsed_url = urllib.parse.urlparse(self.path)
path = parsed_url.path
query_params = urllib.parse.parse_qs(parsed_url.path.split('?')[0] if '?' in self.path else '')
if path == "/upload":
self.handle_upload()
elif path == "/delete":
query_params = urllib.parse.parse_qs(self.path.split('?')[1] if '?' in self.path else '')
filename = query_params.get('file', [None])[0]
if filename:
self.handle_delete(filename)
else:
self.send_error(400, "Missing file parameter")
else:
self.send_error(404, "Not found")
except Exception as e:
logger.error(f"POST error: {e}")
self.send_error(500, "Internal server error")
def serve_index(self):
"""Serve the main HTML page"""
self.send_response(200)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(HTML_TEMPLATE.encode('utf-8'))
def serve_file_list(self):
"""Serve JSON list of files"""
files = get_file_list()
response = {
"files": files,
"total_files": len(files),
"upload_dir": os.path.abspath(UPLOAD_DIR)
}
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(response).encode('utf-8'))
def serve_download(self, filename):
"""Serve file download"""
# Security: prevent directory traversal
if ".." in filename or filename.startswith("/"):
self.send_error(403, "Forbidden")
return
filepath = os.path.join(UPLOAD_DIR, filename)
if not os.path.exists(filepath) or not os.path.isfile(filepath):
self.send_error(404, "File not found")
return
try:
with open(filepath, 'rb') as f:
file_size = os.path.getsize(filepath)
self.send_response(200)
self.send_header("Content-type", get_content_type(filename))
self.send_header("Content-Disposition", f'attachment; filename="{filename}"')
self.send_header("Content-Length", str(file_size))
self.end_headers()
# Send file in chunks
chunk_size = 8192
while True:
chunk = f.read(chunk_size)
if not chunk:
break
self.wfile.write(chunk)
logger.info(f"Downloaded: {filename}")
except Exception as e:
logger.error(f"Download error: {e}")
self.send_error(500, "Error downloading file")
def serve_status(self):
"""Serve server status"""
files = get_file_list()
total_size = sum(f['size'] for f in files)
status = {
"status": "online",
"timestamp": datetime.now().isoformat(),
"total_files": len(files),
"total_size": total_size,
"upload_dir": UPLOAD_DIR,
"https_port": HTTPS_PORT
}
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(status).encode()) #encode('utf-8')
def handle_upload(self):
"""Handle file upload"""
try:
content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0:
# Parse multipart form data
content_type = self.headers.get('Content-Type', '')
if 'multipart/form-data' in content_type:
# Extract boundary
boundary = content_type.split("boundary=")[1].encode()
# Read data
body = self.rfile.read(content_length)
# Simple multipart parser
parts = body.split(b'--' + boundary)
for part in parts:
if b'filename=' in part:
# Extract filename
try:
filename_start = part.find(b'filename="') + 10
filename_end = part.find(b'"', filename_start)
filename = part[filename_start:filename_end].decode()
# Extract file data
data_start = part.find(b'\r\n\r\n') + 4
data_end = part.rfind(b'\r\n')
file_data = part[data_start:data_end]
# Save file
filepath = os.path.join(UPLOAD_DIR, filename)
with open(filepath, 'wb') as f:
f.write(file_data)
logger.info(f"Uploaded: {filename} ({len(file_data)} bytes)")
except Exception as e:
logger.error(f"Upload parse error: {e}")
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "success"}).encode())
else:
self.send_error(400, "No file provided")
except Exception as e:
logger.error(f"Upload error: {e}")
self.send_error(500, "Upload failed")
def handle_delete(self, filename):
"""Handle file deletion"""
# Security: prevent directory traversal
if ".." in filename or filename.startswith("/"):
self.send_error(403, "Forbidden")
return
filepath = os.path.join(UPLOAD_DIR, filename)
if not os.path.exists(filepath):
self.send_error(404, "File not found")
return
try:
os.remove(filepath)
logger.info(f"Deleted: {filename}")
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "success"}).encode())
except Exception as e:
logger.error(f"Delete error: {e}")
self.send_error(500, "Delete failed")
# ============================================================================
# Server Initialization
# ============================================================================
def create_https_server():
"""Create and configure HTTPS server"""
server = HTTPServer((HOST, HTTPS_PORT), FileTransferHandler)
# Create SSL context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=SERVER_CERT, keyfile=SERVER_KEY)
# Optional: Improve security
context.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
context.minimum_version = ssl.TLSVersion.TLSv1_2
# Wrap socket
server.socket = context.wrap_socket(server.socket, server_side=True)
return server
def create_http_server():
"""Create plain HTTP server (for redirects if needed)"""
server = HTTPServer((HOST, HTTP_PORT), FileTransferHandler)
return server
def main():
"""Main entry point"""
print("=" * 70)
print(" HTTPS File Transfer Server")
print("=" * 70)
# Check requirements
if not ensure_ssl_certs():
print("\n❌ SSL certificates not found. Please generate them first.")
return
ensure_upload_dir()
# Create servers
https_server = create_https_server()
logger.info("=" * 70)
logger.info("File Transfer Server Starting")
logger.info("=" * 70)
logger.info(f"📁 Upload directory: {os.path.abspath(UPLOAD_DIR)}")
logger.info(f"🔒 HTTPS port: {HTTPS_PORT}")
logger.info(f"📝 Log file: server.log")
logger.info("=" * 70)
print(f"\n✅ Server starting...")
print(f"📱 Access from iPhone: https://localhost:{HTTPS_PORT}")
print(f"🌐 Access from network: https://<router-ip>:{HTTPS_PORT}")
print(f"\n⚠️ Self-signed certificate - tap 'Continue' on first visit")
print(f"📁 Files uploaded to: {os.path.abspath(UPLOAD_DIR)}")
print(f"📝 Logs: server.log\n")
try:
https_server.serve_forever()
except KeyboardInterrupt:
logger.info("Server shutting down...")
https_server.shutdown()
print("\n✅ Server stopped")
if __name__ == "__main__":
main()

33
ssl-dir/rootCA.crt Normal file
View file

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFqTCCA5GgAwIBAgIUUSKIG/YEujhgnoH985AhHX8FXB4wDQYJKoZIhvcNAQEL
BQAwZDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9z
Y293MQswCQYDVQQKDAJNeTEmMCQGA1UEAwwdQWZpbmEgYW5kIEFydHlvbSBMdGQu
IFJvb3QgQ0EwHhcNMjYwNTE0MjE0MjA3WhcNMzAwNjIyMjE0MjA3WjBkMQswCQYD
VQQGEwJSVTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxCzAJBgNV
BAoMAk15MSYwJAYDVQQDDB1BZmluYSBhbmQgQXJ0eW9tIEx0ZC4gUm9vdCBDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ92j+S7CKOc2c1AiSiLG8q7
p4wdwK8pdLkD1J0suK/EFxgNRLVFlP5EvTHrnJ5AyjkSvGeChInn47wEn8Qvf6BP
gx53REHRvD+tlONm/bMiMyBABAy9YYmEOJrFhYw2LR008Mhxly9sgQGxD6fahEXy
SvnfF7giPk0uWh7chYPQst07qdW4usNvHhWse38VGlZPrsf9M/MA7EyEEtDapWPt
o2/0W3Bk/ufRNdMJf6yrAYYOfYgmW8bVhvfKqIYYAinnTlD2OkUSGc74bsUiay91
oz880kACsiG7sQVjM3K/5ZvDgIKCd7QdGGcCq4vmyBriLEwyetYxba2B+k8JCB+G
6b4YoZHQ63q9XV5ygDB5445WuzByLitVVE+4yUwAfvtXQkFZEZrO6IkMYFuIJxQL
9XeObx0xLcViQeCArvb7a11xvPXj/5whKsFrhrbuyTQFxD8JNTnsZHpw3CnOPJy3
9/TmGQ8U/Mf+UxYX7Olj4J1ItHN1wvpHbRtUFGFz27Ygi5gIiT/RhXz3izWevDvU
0koUand/zzhSt5LwUlepixeRTpnlZTBe64hy5di/saKF4JbUKpel2JqIDauvbHlA
5wmsS87mi4/VIiVCMpBEmZmtIAK+OzQu8WucSE3nMDQkOSNN6/MS9xfW5IvcFV2G
chFrH59fc6MYi7bAdgxTAgMBAAGjUzBRMB0GA1UdDgQWBBQpHtv69SzHowfdbnad
oGrK4gcNbjAfBgNVHSMEGDAWgBQpHtv69SzHowfdbnadoGrK4gcNbjAPBgNVHRMB
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQApd0hAchvf/JElD2yuGsdXV2mw
FaQ7ANmeVjF9ezFXe5BeqsUpdmwX9SgFMJ4kMBM9TMrinANhVudf6lq3/kujNzvu
cB8PMQ5wjWKuMUFNGTibTqLiYRqRU5Mcr1NbsbF9p0D7TLMC8M+ZapmAdeosh6Mz
h8/kNju1VtdHBYR2YyTonuHr6RKt9qNRxZidco11nNUxhIIqof0mAaPyrjYqxhNU
FEyRVRE7S+e06p4JeiWI+JOuyZgy/8yLRNZyaslK7/6lpQU4x1xP9lugNYoHILge
0dVqiPxla5tYthomWwTDgCqQ/6Ogw7ilnC3aCpYiXvp4X/Ekt2sAZng6r9GfMOME
sa1S8z6X7WratEfSl1fiOF5paRVoxuFb76dNPGpfsb7vxivqgimUJzoVYV3tlmSB
TE/85X8wrCXjAcJv/iP+V1IgzyexIVkxXLYg19UB+ovQ4RGcufhN3ywWA9n1z7nT
8x0YTNdR+flVw3FrzM+X06Y7JLhVFUIBfQRtPTciYbc9TFSVhlXfGBLj3/TLG2Bt
KLrON5sH5oXofFYX7gyl+MQV5ya7dJby5b8LEFDY+9u8YY08ymzmaZRCgQaAHb7l
T0jlPknJZwNx6306w6OzIBekrEeVJLiwUC2MNZxOjtDxcpVnwyJVhIrVcB2aPmpe
P+A/5UbpCkkTtGLMsA==
-----END CERTIFICATE-----

26
ssl-dir/server.conf Normal file
View file

@ -0,0 +1,26 @@
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = RU
ST = Moscow
L = Moscow
O = My
CN = Afina and Artyom Ltd.
[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[v3_req]
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.1.111

28
ssl-dir/server.crt Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIExjCCAq6gAwIBAgIUfoDCACbr/m52l9pLX+jnRtJvdK4wDQYJKoZIhvcNAQEL
BQAwZDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9z
Y293MQswCQYDVQQKDAJNeTEmMCQGA1UEAwwdQWZpbmEgYW5kIEFydHlvbSBMdGQu
IFJvb3QgQ0EwHhcNMjYwNTE0MjE0MzAzWhcNMjcwNTE0MjE0MzAzWjBcMQswCQYD
VQQGEwJSVTEPMA0GA1UECAwGTW9zY293MQ8wDQYDVQQHDAZNb3Njb3cxCzAJBgNV
BAoMAk15MR4wHAYDVQQDDBVBZmluYSBhbmQgQXJ0eW9tIEx0ZC4wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1Be3jL7dmATj8tJh3ChWWj2uJvLvVZo0z
jIktQ6d0gU2Y19UYy7ZIU/NyqrnM7E7USPm40C1YaYd5oyYGClzbARChTyylz7YN
LduU/8z2ED3F0dcyuxKFEtSTQnhvc8FyTKKZiy0/DjAyW4rY9ROn+PdwXccpI5k0
azNnh7A1+eKyKEVLYIEFi08fDwpZNKh7rCWLYK3UydpJMedi+/rTK8CJmD5ZVZXK
fWadqKevEb49C7G3Ne0O38q/ILHy6fOM+ic0UQAr5CnGIRaRWoiaqRHkkGSpujS8
04f92ZHXV3LAkUm+cUsD1qMXXxc9EukSlUvrpFvk/TNIFtFDcRfRAgMBAAGjeDB2
MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAG
hwTAqAFvMB0GA1UdDgQWBBSthNDeS2eunXESe5OaChg36D07FDAfBgNVHSMEGDAW
gBQpHtv69SzHowfdbnadoGrK4gcNbjANBgkqhkiG9w0BAQsFAAOCAgEAnEvznC5J
7h7b8xqmUK2+SzfB3FSif56Fcqx+pJJHGvtrcNIUVjfjMIbHjdL+exdD5/wGgrD5
03Mgm0g87PNBBa9H1g+Uw5ZLGVgNkiaSoqzIzuG1XD5dRJ9GVyHqadOlyLju3CoH
ILnbrqH68KcOS1Tnxg91jOkN2UwyZXgJefb5Vu7Qn9rzeJ6ZYv1YU6daJoXbu8A9
vlGQkSBYYZGtlrmpzBYSRsNsRnCArij8N7g2XcuLvwwhhrlN7tVv2ywNeccsIbuL
GiismeuVxHj2vXANAHgFhltXIGimv7ytqjPDV/7Ek5FZT/FhKK3d4w04shGwtkCx
e0Joqj+zWk6yFPq1DblGLEUnQ9IWCwUcD2tVNY38reRRtudgH2fbH5PRbEFMGLbL
wlGzSQaQpblhV0IVUdSf+tDso2aUorF+T3qzx4TZlcPKxmyuhiKDs8pelnSvJp1T
Os2DvYoak87jzJ5hSXrVadJlvtOMDsam9MQCyz28U8nY5u7jxbQrVu53/uRfwRJr
5xNK6qn4sAy/1eudHnF08IopVcoKh3GAnvlVMcx18ZtkurMpM9unSd5FtLBwOS4u
eKMGhr/VE3fqNce66ELRIljrTUWZhs2NtuQXYYhXR2QbXNZY/ousjySiwXc2CKPj
WNqcLqoKoi1i++JT2wsBH7lRZUx1z9DI61A=
-----END CERTIFICATE-----