19 KiB
Code Review: Lab7-Prog
Все замечания сгруппированы по важности. Для каждого указано: файл, строка, почему плохо и как исправить.
🔴 CRITICAL — Требует немедленного исправления
1. server/.../commands/RemoveByIdCommand.kt:36 — Команда remove_by_id полностью сломана
Проблема: repository.deleteByOwner(id) вызывается с ID элемента, а не с ID владельца.
// Было (строка 36):
repository.deleteByOwner(id) // id — UUID элемента, а не владельца!
// Должно быть:
repository.deleteByOwner(ownerId)
Почему плохо: В БД ищутся записи, где owner_id == uuid_элемента. UUID элемента никогда не совпадёт с UUID владельца, поэтому deleteByOwner всегда возвращает 0 удалённых строк. Команда никогда ничего не удаляет.
Как исправить: Заменить id на ownerId (или existing.ownerId).
2. server/.../database/Repository.kt:120-124 — Обновление CarTable использует ID координат вместо ID машины
Проблема: При обновлении записи в CarTable используется UUID из колонки coordinates, а не из колонки car.
// Было (строки 120-121):
val cordsInBeing = row[HumanBeingTable.coordinates] // это UUID координат!
CarTable.update({ CarTable.id eq cordsInBeing }) // но мы ищем Car по этому UUID!
// Должно быть:
val carId = row[HumanBeingTable.car] // UUID машины
CarTable.update({ CarTable.id eq carId }) {
it[CoolTable.cool] = item.car.cool
}
Почему плохо: Поле cool в машине никогда не обновляется. Вместо этого модифицируется (или создаётся) запись в CarTable с UUID, который совпадает с UUID координат. Это портит данные.
Как исправить: Использовать row[HumanBeingTable.car] вместо cordsInBeing.
3. server/.../auth/JWTManager.kt:14 — Секрет JDT — "random"
Проблема: Ключ подписи JWT — строка "random", которая жёстко зашита в коде.
Почему плохо: Любой, кто прочитает код, может подделать JWT-токен и войти под любым пользователем. Это полная компрометация аутентификации.
Как исправить: Вынести в переменную окружения (JWT_SECRET) или конфигурационный файл. Генерировать криптостойкий ключ:
val secret = System.getenv("JWT_SECRET") ?: error("JWT_SECRET not set")
4. server/.../database/DatabaseManager.kt:15-18 — Учётные данные БД жёстко зашиты в коде
Проблема: jdbcUrl, username = "mainUser", password = "superDuperPassword" в открытом виде.
Почему плохо: Адрес сервера (aklivtsov.tech:5430) и пароль доступны любому, у кого есть доступ к репозиторию. Злоумышленник может подключиться напрямую к PostgreSQL и прочитать/изменить все данные.
Как исправить: Использовать переменные окружения:
val dbUrl = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/prog7_db"
val dbUser = System.getenv("DB_USER") ?: error("DB_USER not set")
val dbPass = System.getenv("DB_PASS") ?: error("DB_PASS not set")
🟠 HIGH — Критические баги и проблемы безопасности
5. common/.../models/HumanBeing.kt:27 — id объявлен как var в data class
Проблема: var id: UUID? = null — мутабельное поле в data class. Команда AddCommand изменяет его после создания: secured.id = repository.write(secured).
Почему плохо: Если объект с id=null добавлен в коллекцию, а потом id меняется, это ломает контракты hashCode()/equals() и TreeSet. Объект может "потеряться" в коллекции.
Как исправить: Сделать id иммутабельным. Использовать copy(id = ...) вместо прямой мутации.
6. common/.../models/HumanBeing.kt:50-53 — Сравнение UUID как строк
Проблема: this.id.toString().compareTo(other.id.toString()) — лексикографическое сравнение строковых представлений UUID.
Почему плохо: UUID "10000000-..." будет меньше "99999999-..." как строка, хотя как число он больше. Порядок сортировки сломан.
Как исправить: Использовать this.id!!.compareTo(other.id!!) (сравнение по битовому представлению UUID) или обработать null.
7. common/.../models/HumanBeing.kt:65 — NPE при hashCode() с null id
Проблема: id.hashCode() — на nullable типе это эквивалентно id!!.hashCode(). Если id == null — будет NullPointerException.
Как исправить: id?.hashCode() ?: 0
8. server/.../collection/CollectionManager.kt:34-43 — Гонки данных (data race)
Проблема:
removeById():getById()внутриsynchronized, но самcollection.remove()— вне синхронизации.clear(): вызывается вообще без синхронизации.
Почему плохо: При конкурентном доступе: ConcurrentModificationException, потеря данных, чтение несогласованного состояния.
Как исправить: Все публичные методы должны быть обёрнуты в synchronized(collection) { ... }.
9. server/.../commands/RemoveByIdCommand.kt:33 — Сравнение UUID как строк (2)
Проблема: existing.ownerId.toString() != userId — строковое сравнение. userId из JWT совпадает по смыслу, но типобезопасность потеряна.
Как исправить: Хранить userId как UUID и сравнивать: existing.ownerId == UUID.fromString(userId).
10. client/.../network/NetworkManager.kt:77 — " no id" с пробелом
Проблема: private var id: String = " no id" — если getUUID() вызван до аутентификации, UUID.fromString(" no id") упадёт с IllegalArgumentException.
Как исправить: Использовать null и UUID?:
private var id: UUID? = null
fun getUUID(): UUID = id ?: error("Not authenticated")
11. client/.../network/NetworkManager.kt:78 — Не проверяется возврат DatagramChannel.write()
Проблема: Метод write() возвращает количество записанных байт. Для UDP при превышении MTU часть данных может быть отброшена, но код не проверяет, что все байты отправлены.
Как исправить: Использовать DatagramChannel.send() с готовым DatagramPacket, или проверять возвращаемое значение.
12. client/.../input/IOManager.kt:17-19 — Падение при отсутствии терминала
Проблема: TerminalBuilder.builder().system(true).build() упадёт с исключением, если stdin не терминал (CI/CD, пайп, Docker без TTY).
Как исправить: Обработать исключение и упасть на Scanner(System.in).
13. client/.../commands/AuthCommand.kt:19-20 — ArrayIndexOutOfBoundsException
Проблема: Проверяется только args.isEmpty(), но не args.size < 3. Если пользователь введёт auth login, то args = ["login"] (size = 1), и args[1] вызовет crash.
Как исправить: args.size < 3 → вернуть ошибку.
14. client/.../commands/ExecuteScriptCommand.kt:13 — Та же проблема
Проблема: args[0] без проверки args.isNotEmpty().
Как исправить: Проверить args.isNotEmpty().
15. server/.../commands/SyncCommand.kt:41 — Обсценная лексика в ответе сервера
Проблема: message = "хуй" — нецензурное выражение возвращается клиенту.
Почему плохо: Неприемлемо для любого кода, даже учебного. Это будет видно пользователю при синхронизации команд.
Как исправить: Заменить на "Commands synchronized" или "Команды синхронизированы".
16. client/.../commands/SyncCommand.kt:24 — Отладочный print(res) в продакшене
Проблема: В коде остался print(res), который выводит сырой Response-объект в stdout.
Как исправить: Удалить строку.
17. server/.../collection/CollectionManager.kt:34-38 — TOCTOU race в removeById
Выше описано (#8), но это отдельный опасный случай: между проверкой getById() и удалением коллекция может измениться.
18. server/.../database/tables/UserTable.kt:7 — Комментарий // change про пароль
Автор сам знает, что пароль хранится небезопасно (MD5 без соли). MD5 неприемлем для хранения паролей — взламывается миллиардами хэшей в секунду на обычном GPU.
Как исправить: Использовать bcrypt/Argon2.
19. client/.../commands/AuthCommand.kt:38-41 — MD5 + pass-the-hash
Проблема: MD5-хэш отправляется по сети и используется сервером как пароль. Перехватчик трафика получает готовый хэш, который можно просто отправить серверу (pass-the-hash).
Как исправить: Использовать TLS/DTLS для транспорта. Сервер должен хэшировать пароль повторно с солью.
20. gateway/.../core/GatewayManager.kt:55-64, 120-127 — Утечка сокетов
Проблема: При ошибке в probeServer() или ProxyTask.compute() DatagramSocket не закрывается. При каждом неудачном health check утекает файловый дескриптор.
Как исправить: Использовать try {} finally { socket.close() } или use {}.
🟡 MEDIUM — Важно, но не критично
| # | Файл | Строка | Проблема | Исправление |
|---|---|---|---|---|
| 21 | server/.../NetworkManager.kt:24-25 |
Использование ForkJoinPool для I/O | Заменить на ExecutorService/thread pool | |
| 22 | server/.../NetworkManager.kt:88 |
Ошибка сервера возвращает e.message клиенту |
Логировать на сервере, клиенту — "Внутренняя ошибка" | |
| 23 | server/.../commands/UpdateCommand.kt:38 |
ownerId в теле запроса может отличаться от JWT (теоретически) |
Принудительно устанавливать ownerId из JWT в команде |
|
| 24 | server/.../repository/Repository.kt:146-156 |
Мёртвый метод delete(id: UUID) — нигде не вызывается |
Удалить, или реализовать корректно (с удалением coordinates/car) | |
| 25 | server/.../repository/Repository.kt:162 |
Пароль сравнивается напрямую в SQL-запросе | Использовать server-side hashing с солью | |
| 26 | server/.../database/DatabaseManager.kt:44-54 |
Мёртвый метод test() |
Удалить | |
| 27 | server/.../database/tables/CoordinatesTable.kt:5-6 |
Колонки NULLABLE в БД, NON-NULL в модели | Добавить NOT NULL в DDL |
|
| 28 | server/.../database/tables/CarTable.kt:6 |
То же | ||
| 29 | server/.../runner/CommandInvoker.kt:51-52 |
getHistory() не синхронизирован |
Синхронизировать или использовать CopyOnWriteArrayList |
|
| 30 | server/.../commands/ClearCommand.kt:26-27 |
TOCTOU между getAll() и итерацией |
Блокировать коллекцию на всё время операции | |
| 31 | server/.../auth/JWTManager.kt:38-49 |
validateJWT не использует параметр userId |
Проверять, что subject токена == ожидаемому userId | |
| 32 | client/.../runner/CommandInvoker.kt:45-47 |
Ловится только IllegalStateException |
Добавить общий catch для непредвиденных ошибок | |
| 33 | client/.../runner/CommandInvoker.kt:7 |
Импортировано 2 разных logging-библиотеки | Оставить только io.github.oshai.kotlinlogging |
|
| 34 | client/.../input/IOManager.kt:15 |
isInteractive всегда true |
Отключать интерактивный режим при execute_script |
|
| 35 | client/.../input/HumanBeingBuilder.kt:65-165 |
Массовое дублирование в read-методах | Вынести в generic-функцию | |
| 36 | client/.../input/HumanBeingBuilder.kt:171-173 |
Мёртвый метод printPrompt |
Удалить или использовать | |
| 37 | gateway/.../core/GatewayManager.kt:55,120 |
Создание нового DatagramSocket на каждый запрос |
Переиспользовать сокеты | |
| 38 | gateway/.../core/GatewayManager.kt:135-142 |
Бесконечные ретраи при флапе бэкендов | Добавить лимит retry |
🟢 LOW — Косметика, стиль, конвенции
| # | Файл | Строка | Проблема | Исправление |
|---|---|---|---|---|
| 39 | common/.../network/AppJson.kt:5 |
AppJson — нейминг нарушает конвенции Kotlin |
APP_JSON (UPPER_SNAKE_CASE) |
|
| 40 | common/.../network/AuthResponse.kt:6 |
Лишний пробел в class AuthResponse ( |
class AuthResponse( |
|
| 41 | common/.../models/Coordinates.kt:11-12 |
Пустая строка между KDoc и аннотацией | Убрать пустую строку | |
| 42 | common/.../commands/CommandArgType.kt:5 |
Комментарий // why there's only two types? yes. |
Удалить | |
| 43 | server/.../Main.kt:28 |
Комментарий // let's pretend that i'm in |
Удалить | |
| 44 | client/.../Main.kt:16 |
Комментарий // Name by Boris Bosenko |
Удалить | |
| 45 | client/.../Main.kt:47 |
Пропущен пробел после запятой | invoker: CommandInvoker, client: NetworkManager |
|
| 46 | client/.../input/IOManager.kt:73 |
print() вызывает println() |
Переименовать или исправить | |
| 47 | client/.../input/IOManager.kt:27 |
Закомментированный код | Удалить или раскомментировать | |
| 48 | server/.../commands/AddIfMaxCommand.kt:13 |
val manager не private |
Добавить private |
|
| 49 | server/.../commands/ClearCommand.kt:14 |
var repository вместо val |
Заменить на val |
|
| 50 | Все server/commands | @Serializable + @Contextual на классах команд — не используются |
Удалить (минимум 10 файлов) | |
| 51 | Везде | Нет форматтера (ktlint, spotless) | Добавить плагин в Gradle | |
| 52 | Везде | Нет тестов | Написать хотя бы unit-тесты на команды | |
| 53 | gateway/.../core/GatewayManager.kt:73 |
AtomicInteger переполнится через ~2 млрд запросов |
Использовать AtomicLong или обнуление |
📋 Итого по категориям
| Категория | Кол-во | Самые важные |
|---|---|---|
| 🔴 Critical | 4 | RemoveByIdCommand сломан (#1), CarTable не обновляется (#2), JWT secret (#3), credentials в коде (#4) |
| 🟠 High | 16 | var id (#5), сравнение UUID (#6), NPE в hashCode (#7), data races (#8), утечка сокетов (#20), мат в ответе (#15), MD5 (#14) |
| 🟡 Medium | 18 | Мёртвый код, ForkJoinPool для I/O, утечка деталей в ошибках, отсутствие тестов |
| 🟢 Low | 15 | Косметика, нейминг, комментарии, стиль |
💡 Рекомендации по очерёдности исправления
- Сначала: Critical #1, #2 — баги, делающие команды нерабочими
- Сразу после: Critical #3, #4 + High #14, #15 — безопасность
- Затем: High #5, #6, #7, #8 — стабильность и корректность данных
- Потом: Остальные Medium/Low — по желанию
Топ-5 исправлений, которые дадут 80% улучшения:
RemoveByIdCommand.kt:36—repository.deleteByOwner(id)→repository.deleteByOwner(ownerId)Repository.kt:120-124— заменитьcordsInBeingнаcarId- Вынести секреты (JWT, БД) в переменные окружения
- Синхронизировать
CollectionManager(removeById, clear, size) - Удалить
@Serializable/@Contextualсо всех серверных команд + мёртвый код