Update public source

This commit is contained in:
2026-10-06 21:08:12 +03:00
commit f231cc5b1f
102 changed files with 48249 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
from flask import Blueprint
cracklab_bp = Blueprint(
"cracklab",
__name__,
url_prefix="/cracklab",
template_folder="templates",
static_folder="static",
static_url_path="/static",
)
from cracklab.server.web import routes # noqa: E402,F401
+408
View File
@@ -0,0 +1,408 @@
from __future__ import annotations
import json
from collections import Counter
from flask import redirect, render_template, request, url_for
from cracklab.server.db.connection import connect_database
from cracklab.server.repositories.clients import ClientsRepository
from cracklab.server.repositories.jobs import JobsRepository
from cracklab.server.repositories.methods import MethodsRepository
from cracklab.server.repositories.results import ResultsRepository
from cracklab.server.services.methods import MethodService, MethodServiceError
from cracklab.server.web import cracklab_bp
@cracklab_bp.route("/")
def dashboard():
conn = connect_database()
try:
methods = MethodsRepository(conn).list_methods()
jobs = JobsRepository(conn).list_jobs()
clients = ClientsRepository(conn).list_clients()
results = ResultsRepository(conn).list_results()
finally:
conn.close()
job_statuses = Counter(
row["status"]
for row in jobs
)
recent_jobs = jobs[:10]
method_names = {
row["id"]: row["name"]
for row in methods
}
client_names = {
row["id"]: row["name"]
for row in clients
}
return render_template(
"dashboard.html",
active_page="dashboard",
methods_count=len(methods),
jobs_count=len(jobs),
clients_count=len(clients),
results_count=len(results),
active_jobs_count=sum(
job_statuses.get(status, 0)
for status in (
"CREATED",
"QUEUED",
"RUNNING",
"PARTIAL",
)
),
job_statuses=job_statuses,
recent_jobs=recent_jobs,
method_names=method_names,
client_names=client_names,
)
@cracklab_bp.route("/jobs/")
def jobs_page():
return _placeholder(
"Jobs",
"Job management will be implemented here.",
"jobs",
)
@cracklab_bp.route("/methods/")
def methods_page():
conn = connect_database()
try:
service = MethodService(conn)
methods = service.list_methods()
method_rows = []
for method in methods:
versions = service.list_method_versions(method["id"])
latest = versions[-1] if versions else None
steps_count = 0
if latest is not None:
try:
definition = json.loads(
latest["definition_json"]
)
steps_count = len(
definition.get("steps", [])
)
except (TypeError, ValueError):
steps_count = 0
method_rows.append(
{
"id": method["id"],
"name": method["name"],
"description": method["description"],
"created_at": method["created_at"],
"version": (
latest["version"]
if latest is not None
else None
),
"steps_count": steps_count,
}
)
finally:
conn.close()
return render_template(
"methods.html",
active_page="methods",
methods=method_rows,
)
@cracklab_bp.route("/methods/create/", methods=["GET", "POST"])
def method_create_page():
error = None
if request.method == "POST":
try:
name = request.form.get("name", "").strip()
description = request.form.get(
"description",
"",
).strip()
hash_mode_raw = request.form.get(
"hash_mode",
"22000",
).strip()
if not name:
raise MethodServiceError(
"Введите название метода."
)
if hash_mode_raw != "22000":
raise MethodServiceError(
"Сейчас поддерживается только hash mode 22000."
)
hash_mode = 22000
steps = _build_steps_from_form()
definition = {
"schema_version": 1,
"hash_mode": hash_mode,
"steps": steps,
}
conn = connect_database()
try:
service = MethodService(conn)
with conn:
method_id = service.create_method(
name=name,
description=description or None,
)
service.create_method_version(
method_id=method_id,
version=1,
definition=definition,
)
finally:
conn.close()
return redirect(
url_for(
"cracklab.methods_page",
)
)
except (MethodServiceError, ValueError) as exc:
error = str(exc)
return render_template(
"method_create.html",
active_page="methods",
error=error,
)
def _build_steps_from_form() -> list[dict]:
step_count_raw = request.form.get(
"step_count",
"1",
).strip()
try:
step_count = int(step_count_raw)
except ValueError as exc:
raise MethodServiceError(
"Некорректное количество шагов."
) from exc
if step_count < 1:
raise MethodServiceError(
"Метод должен содержать хотя бы один шаг."
)
steps = []
for index in range(1, step_count + 1):
prefix = f"step_{index}"
attack_mode_raw = request.form.get(
f"{prefix}_attack_mode",
"",
).strip()
if not attack_mode_raw:
raise MethodServiceError(
f"Для шага {index} не выбран attack mode."
)
try:
attack_mode = int(attack_mode_raw)
except ValueError as exc:
raise MethodServiceError(
f"Некорректный attack mode в шаге {index}."
) from exc
if attack_mode not in (0, 1, 3, 6, 7):
raise MethodServiceError(
f"Attack mode {attack_mode} "
f"не поддерживается клиентом CrackLab."
)
step = {
"step_id": f"step-{index:03d}",
"attack_mode": attack_mode,
}
workload_raw = request.form.get(
f"{prefix}_workload_profile",
"2",
).strip()
if workload_raw:
try:
workload_profile = int(workload_raw)
except ValueError as exc:
raise MethodServiceError(
f"Некорректный workload profile "
f"в шаге {index}."
) from exc
if workload_profile not in (1, 2, 3, 4):
raise MethodServiceError(
f"Workload profile в шаге {index} "
"должен быть от 1 до 4."
)
step["workload_profile"] = workload_profile
if attack_mode == 0:
step["dictionary"] = {
"resource": _get_dictionary(
prefix,
"dictionary",
index,
),
}
elif attack_mode == 1:
step["dictionaries"] = [
{
"resource": _get_dictionary(
prefix,
"dictionary_1",
index,
),
},
{
"resource": _get_dictionary(
prefix,
"dictionary_2",
index,
),
},
]
elif attack_mode == 3:
step["mask"] = _get_mask(
prefix,
index,
)
elif attack_mode == 6:
step["dictionary"] = {
"resource": _get_dictionary(
prefix,
"dictionary",
index,
),
}
step["mask"] = _get_mask(
prefix,
index,
)
elif attack_mode == 7:
step["mask"] = _get_mask(
prefix,
index,
)
step["dictionary"] = {
"resource": _get_dictionary(
prefix,
"dictionary",
index,
),
}
steps.append(step)
return steps
def _get_dictionary(
prefix: str,
field: str,
index: int,
) -> str:
value = request.form.get(
f"{prefix}_{field}",
"",
).strip()
if not value:
raise MethodServiceError(
f"Для шага {index} необходимо указать словарь."
)
if not value.startswith("wordlist:"):
value = f"wordlist:{value}"
return value
def _get_mask(
prefix: str,
index: int,
) -> str:
value = request.form.get(
f"{prefix}_mask",
"",
).strip()
if not value:
raise MethodServiceError(
f"Для шага {index} необходимо указать маску."
)
return value
@cracklab_bp.route("/clients/")
def clients_page():
return _placeholder(
"Clients",
"Client management will be implemented here.",
"clients",
)
@cracklab_bp.route("/results/")
def results_page():
return _placeholder(
"Results",
"Result history will be implemented here.",
"results",
)
def _placeholder(
title: str,
description: str,
active_page: str,
):
return render_template(
"placeholder.html",
active_page=active_page,
title=title,
description=description,
)
+785
View File
@@ -0,0 +1,785 @@
@font-face {
font-family: "Bender";
src:
url("fonts/Bender.woff2") format("woff2"),
url("fonts/Bender.woff") format("woff");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Bender";
src:
url("fonts/Bender-Bold.woff2") format("woff2"),
url("fonts/Bender-Bold.woff") format("woff");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "Bender";
src:
url("fonts/Bender-Light.woff2") format("woff2"),
url("fonts/Bender-Light.woff") format("woff");
font-weight: 300;
font-style: normal;
}
:root {
--bg: #0f141a;
--surface: #171e26;
--surface-light: #1c252f;
--border: #2b3743;
--text: #e6edf3;
--muted: #8b98a6;
--green: #45d483;
--yellow: #e5c07b;
--red: #ef6262;
--accent: #69a7ff;
--terminal: #0a0f14;
}
* {
box-sizing: border-box;
}
html {
min-height: 100%;
}
body {
margin: 0;
padding: 0;
min-height: 100vh;
font-family:
"Bender",
Arial,
Helvetica,
sans-serif;
background: var(--bg);
color: var(--text);
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
color: #9bc4ff;
}
.cracklab-header {
position: sticky;
top: 0;
z-index: 1000;
width: 100%;
background: rgba(15, 20, 26, 0.96);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(12px);
}
.cracklab-header-inner {
width: 100%;
max-width: 1800px;
margin: 0 auto;
min-height: 68px;
display: flex;
align-items: center;
gap: 36px;
padding: 0 30px;
}
.cracklab-brand {
flex: 0 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--text);
text-decoration: none;
white-space: nowrap;
}
.cracklab-brand:hover {
color: var(--text);
text-decoration: none;
}
.cracklab-brand-title {
font-size: 19px;
font-weight: 700;
letter-spacing: 0.4px;
line-height: 1.1;
}
.cracklab-brand-subtitle {
margin-top: 3px;
color: var(--muted);
font-size: 12px;
font-weight: 400;
letter-spacing: 0.7px;
line-height: 1.1;
}
.cracklab-nav {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
}
.cracklab-nav a {
display: inline-flex;
align-items: center;
min-height: 38px;
padding: 0 14px;
border-radius: 7px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.7px;
white-space: nowrap;
transition:
color 0.15s ease,
background 0.15s ease;
}
.cracklab-nav a:hover {
color: var(--text);
background: var(--surface-light);
text-decoration: none;
}
.cracklab-nav a.active {
color: var(--text);
background: var(--surface-light);
box-shadow:
inset 0 -2px 0 var(--accent);
}
.cracklab-content {
width: 100%;
max-width: 1800px;
margin: 0 auto;
padding: 30px;
}
.page-title {
margin-top: 0;
margin-bottom: 8px;
font-size: 34px;
font-weight: 700;
letter-spacing: 0.2px;
}
.page-description {
color: var(--muted);
margin-bottom: 30px;
font-size: 15px;
}
.methods-page {
width: 100%;
}
.methods-page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 30px;
}
.methods-page-header .page-description {
margin-bottom: 0;
}
.methods-button,
.methods-button-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 10px 18px;
border: 2px outset ButtonFace;
border-radius: 7px;
font: inherit;
font-size: 14px;
font-weight: 700;
cursor: pointer;
text-decoration: none;
transition:
background 0.15s ease,
color 0.15s ease,
transform 0.15s ease;
}
.methods-button {
background: var(--accent);
color: #0f141a;
}
.methods-button:hover {
background: #8abaff;
color: #0f141a;
text-decoration: none;
}
.methods-button:active {
border-style: inset;
transform: translateY(1px);
}
.methods-button-secondary {
background: var(--surface-light);
color: var(--text);
}
.methods-button-secondary:hover {
background: var(--border);
color: var(--text);
text-decoration: none;
}
.methods-button-secondary:active {
border-style: inset;
transform: translateY(1px);
}
.methods-list {
display: grid;
gap: 14px;
}
.method-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 30px;
padding: 20px 22px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.20);
}
.method-card-main {
min-width: 0;
}
.method-card-title {
margin-bottom: 7px;
color: var(--text);
font-size: 20px;
font-weight: 700;
}
.method-card-description {
margin-bottom: 9px;
color: var(--muted);
font-size: 14px;
line-height: 1.5;
}
.method-card-id {
color: var(--muted);
font-family: monospace;
font-size: 12px;
}
.method-card-meta {
display: flex;
flex-shrink: 0;
gap: 28px;
}
.method-meta-item {
min-width: 80px;
}
.method-meta-item span {
display: block;
margin-bottom: 5px;
color: var(--muted);
font-size: 12px;
}
.method-meta-item strong {
color: var(--text);
font-size: 14px;
font-weight: 700;
}
.method-empty {
padding: 50px 30px;
text-align: center;
background: var(--surface);
border: 1px dashed var(--border);
border-radius: 12px;
}
.method-empty-title {
margin-bottom: 10px;
color: var(--text);
font-size: 21px;
font-weight: 700;
}
.method-empty-description {
max-width: 520px;
margin: 0 auto 24px;
color: var(--muted);
font-size: 14px;
line-height: 1.6;
}
.stats-grid {
display: grid;
grid-template-columns:
repeat(
auto-fit,
minmax(180px, 1fr)
);
gap: 18px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
text-align: center;
}
.stat-value {
margin-bottom: 8px;
color: var(--accent);
font-size: 34px;
font-weight: 700;
}
.stat-title {
color: var(--muted);
font-size: 14px;
}
.dashboard-grid {
display: grid;
grid-template-columns:
minmax(260px, 0.35fr)
minmax(500px, 1fr);
gap: 24px;
align-items: start;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 22px;
margin-bottom: 24px;
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.20);
}
.card h2 {
margin-top: 0;
margin-bottom: 20px;
color: var(--text);
font-size: 21px;
}
.table-container {
overflow-x: auto;
overflow-y: auto;
max-height: 700px;
border: 1px solid var(--border);
border-radius: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: var(--surface-light);
color: var(--text);
font-weight: 700;
text-align: left;
padding: 12px;
border-bottom: 2px solid var(--border);
position: sticky;
top: 0;
z-index: 2;
}
td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
vertical-align: top;
color: var(--text);
}
tbody tr:hover {
background: rgba(105, 167, 255, 0.05);
}
.status-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.status-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.status-row:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.badge {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 9px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
background: var(--surface-light);
color: var(--muted);
}
.badge-created,
.badge-queued {
color: var(--yellow);
}
.badge-running,
.badge-partial {
color: var(--accent);
}
.badge-completed {
color: var(--green);
}
.badge-failed,
.badge-cancelled {
color: var(--red);
}
.muted {
color: var(--muted);
}
.empty-state {
padding: 35px 20px;
color: var(--muted);
text-align: center;
border: 1px dashed var(--border);
border-radius: 8px;
}
@media (max-width: 900px) {
.cracklab-header-inner {
align-items: flex-start;
flex-direction: column;
gap: 12px;
padding-top: 14px;
padding-bottom: 14px;
}
.cracklab-nav {
width: 100%;
}
.cracklab-content {
padding: 20px;
}
.methods-page-header {
align-items: stretch;
flex-direction: column;
gap: 16px;
}
.methods-button {
width: 100%;
}
.method-card {
align-items: stretch;
flex-direction: column;
gap: 20px;
}
.method-card-meta {
width: 100%;
justify-content: space-between;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.cracklab-content {
padding: 15px;
}
.page-title {
font-size: 28px;
}
.stat-value {
font-size: 28px;
}
.card {
padding: 16px;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+95
View File
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>
{% block title %}
CrackLab
{% endblock %}
</title>
<link
rel="stylesheet"
href="{{ url_for('cracklab.static', filename='cracklab.css') }}"
>
</head>
<body>
<header class="cracklab-header">
<div class="cracklab-header-inner">
<a
class="cracklab-brand"
href="{{ url_for('cracklab.dashboard') }}"
>
<span class="cracklab-brand-title">
WiFi GPS Mapper
</span>
<span class="cracklab-brand-subtitle">
CrackLab
</span>
</a>
<nav class="cracklab-nav">
<a
class="{% if active_page == 'dashboard' %}active{% endif %}"
href="{{ url_for('cracklab.dashboard') }}"
>
Dashboard
</a>
<a
class="{% if active_page == 'jobs' %}active{% endif %}"
href="{{ url_for('cracklab.jobs_page') }}"
>
Jobs
</a>
<a
class="{% if active_page == 'methods' %}active{% endif %}"
href="{{ url_for('cracklab.methods_page') }}"
>
Methods
</a>
<a
class="{% if active_page == 'clients' %}active{% endif %}"
href="{{ url_for('cracklab.clients_page') }}"
>
Clients
</a>
<a
class="{% if active_page == 'results' %}active{% endif %}"
href="{{ url_for('cracklab.results_page') }}"
>
Results
</a>
</nav>
</div>
</header>
<main class="cracklab-content">
{% block content %}
{% endblock %}
</main>
</body>
</html>
@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% block title %}
CrackLab — Dashboard
{% endblock %}
{% block content %}
<h1 class="page-title">
CrackLab
</h1>
<div class="page-description">
Job and cracking coverage management.
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">
{{ methods_count }}
</div>
<div class="stat-title">
Methods
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ jobs_count }}
</div>
<div class="stat-title">
Jobs
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ active_jobs_count }}
</div>
<div class="stat-title">
Active Jobs
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ clients_count }}
</div>
<div class="stat-title">
Clients
</div>
</div>
<div class="stat-card">
<div class="stat-value">
{{ results_count }}
</div>
<div class="stat-title">
Results
</div>
</div>
</div>
<div class="dashboard-grid">
<section class="card">
<h2>
Job Status
</h2>
<div class="status-list">
{% for status in (
"CREATED",
"QUEUED",
"RUNNING",
"PARTIAL",
"COMPLETED",
"FAILED",
"CANCELLED"
) %}
<div class="status-row">
<span>
<span class="badge badge-{{ status|lower }}">
{{ status }}
</span>
</span>
<strong>
{{ job_statuses.get(status, 0) }}
</strong>
</div>
{% endfor %}
</div>
</section>
<section class="card">
<h2>
Recent Jobs
</h2>
{% if recent_jobs %}
<div class="table-container">
<table>
<thead>
<tr>
<th>Job</th>
<th>Method</th>
<th>Status</th>
<th>Hashes</th>
<th>Client</th>
</tr>
</thead>
<tbody>
{% for job in recent_jobs %}
<tr>
<td>
<a
href="{{ url_for(
'cracklab.jobs_page'
) }}"
>
{{ job["id"] }}
</a>
</td>
<td>
{{ method_names.get(
job["method_id"],
job["method_id"]
) }}
<span class="muted">
v{{ job["method_version"] }}
</span>
</td>
<td>
<span
class="badge badge-{{
job["status"]|lower
}}"
>
{{ job["status"] }}
</span>
</td>
<td>
{{ job["hash_count"] }}
</td>
<td>
{% if job["client_id"] %}
{{ client_names.get(
job["client_id"],
job["client_id"]
) }}
{% else %}
<span class="muted">
—
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
No jobs have been issued yet.
</div>
{% endif %}
</section>
</div>
{% endblock %}
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
{% extends "base.html" %}
{% block title %}
CrackLab — Methods
{% endblock %}
{% block content %}
<div class="methods-page">
<div class="methods-page-header">
<div>
<h1 class="page-title">
Methods
</h1>
<div class="page-description">
Рабочие методы перебора, сохранённые в базе данных.
</div>
</div>
{% if methods %}
<a
class="methods-button"
href="{{ url_for('cracklab.method_create_page') }}"
>
+ Создать метод
</a>
{% endif %}
</div>
{% if methods %}
<section class="methods-list">
{% for method in methods %}
<article class="method-card">
<div class="method-card-main">
<div class="method-card-title">
{{ method.name }}
</div>
{% if method.description %}
<div class="method-card-description">
{{ method.description }}
</div>
{% endif %}
<div class="method-card-id">
{{ method.id }}
</div>
</div>
<div class="method-card-meta">
<div class="method-meta-item">
<span>Version</span>
<strong>
{% if method.version %}
{{ method.version }}
{% else %}
—
{% endif %}
</strong>
</div>
<div class="method-meta-item">
<span>Steps</span>
<strong>
{{ method.steps_count }}
</strong>
</div>
<div class="method-meta-item">
<span>Created</span>
<strong>
{{ method.created_at }}
</strong>
</div>
</div>
</article>
{% endfor %}
</section>
{% else %}
<section class="method-empty">
<div class="method-empty-title">
Методов пока нет
</div>
<div class="method-empty-description">
Создайте первый метод, чтобы начать
выдавать задания CrackLab.
</div>
<a
class="methods-button"
href="{{ url_for('cracklab.method_create_page') }}"
>
+ Создать метод
</a>
</section>
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}
CrackLab — {{ title }}
{% endblock %}
{% block content %}
<h1 class="page-title">
{{ title }}
</h1>
<div class="page-description">
{{ description }}
</div>
<section class="card">
<div class="empty-state">
This section is under development.
</div>
</section>
{% endblock %}