Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,3 @@ jobs:

- name: Run Bandit
run: poetry run bandit -r ./server

- name: Run Tests
run: poetry run pytest -s -x --cov=server -vv; poetry run coverage html

- name: Store coverage files
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: htmlcov
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use o IntelliSense para saber mais sobre os atributos possíveis.
// Focalizar para exibir as descrições dos atributos existentes.
// Para obter mais informações, acesse: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Confy Server",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"server.main:app",
"--reload"
],
"jinja": true
}
]
}
38 changes: 6 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,48 +30,22 @@ Mesmo que alguma comunicação seja interceptada na rede, ela é ilegível.

## Executando o servidor

### Via Docker (recomendado)
A maneira mais rápida e fácil de executar o servidor é via docker compose.

A maneira mais rápida e fácil de executar o servidor é com um container [Docker](https://www.docker.com/).

```shell
docker run -d --restart=always -p 8000:8000 --name confy-server henriquesebastiao/confy-server:latest
```

O servidor Confy agora está rodando em [http://0.0.0.0:8000](http://0.0.0.0:8000).

### Localmente

Caso queira executar o servidor sem Docker para fins de debug ou desenvolvimento siga as etapas abaixo.

1. Tenha instalado as seguintes dependências:

- [Git](https://git-scm.com/downloads)
- [Poetry](https://python-poetry.org/docs/#installation)
- [Python 3.13 ou superior](https://www.python.org/downloads/)

2. Clone este repositório e entre na pasta.
1. Clone este repositório e entre na pasta do projeto.

```shell
git clone https://github.com/confy-security/server.git && cd server
```

3. Instale as dependência do servidor com Poetry.

```shell
poetry install
```

4. Ative o ambiente virtual.

5. Execute o servidor.
2. Execute o docker compose.

```shell
task run
docker compose up -d
```

Pronto, agora o servidor Confy agora está rodando em [http://0.0.0.0:8000](http://0.0.0.0:8000).
O servidor Confy agora está rodando em [http://0.0.0.0:9000](http://0.0.0.0:9000).

## License
## Licença

Este projeto está licenciado sob os termos da licença GNU GPL-3.0.
17 changes: 16 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ services:
build: .
volumes:
- .:/code
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
web:
container_name: web-confy-server
image: nginx:stable-alpine
Expand All @@ -17,4 +22,14 @@ services:
environment:
- NGINX_PORT=80
depends_on:
- server
- server

redis:
image: redis:alpine
container_name: confy-redis
restart: always
volumes:
- redis_data:/data

volumes:
redis_data:
222 changes: 40 additions & 182 deletions poetry.lock

Large diffs are not rendered by default.

17 changes: 6 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ dependencies = [
"fastapi[standard] (>=0.116.1,<0.117.0)",
"websockets (>=15.0.1,<16.0.0)",
"uvicorn (>=0.34.3,<0.35.0)",
"psutil (>=5.7.2,<7.0.0)"
"psutil (>=5.7.2,<7.0.0)",
"redis[async] (>=6.4.0,<7.0.0)",
"pydantic-settings (>=2.10.1,<3.0.0)",
]

[tool.poetry.group.dev.dependencies]
ruff = "^0.11.13"
taskipy = "^1.14.1"
radon = "^6.0.1"
bandit = "^1.8.6"
pytest = "^8.4.1"
pytest-cov = "^6.2.1"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
Expand All @@ -44,13 +44,9 @@ quote-style = 'single'
"tests/*.py" = ['D103', 'D100', 'PLR2004']
"server/schemas/*.py" = ['D101', 'D100']
"server/routers/status.py" = ['D100']

[tool.pytest.ini_options]
pythonpath = "."
addopts = '-p no:warnings'

[tool.coverage.run]
branch = true
"settings.py" = ['D103', 'D101', 'D100']
"db.py" = ['D100']
"hasher.py" = ['D100']

[tool.taskipy.tasks]
run = 'uvicorn --reload --host 127.0.0.1 server.main:app'
Expand All @@ -59,4 +55,3 @@ format = 'ruff format .; ruff check . --fix'
radon = 'radon cc ./server -a -na'
bandit = 'bandit -r ./server'
export = './scripts/rm-requirements.sh && poetry export -f requirements.txt --output requirements.txt --without-hashes --without dev'
test = 'pytest -s -x --cov=server -vv; coverage html'
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ markupsafe==3.0.2 ; python_version >= "3.13" and python_version < "4.0"
mdurl==0.1.2 ; python_version >= "3.13" and python_version < "4.0"
psutil==6.1.1 ; python_version >= "3.13" and python_version < "4.0"
pydantic-core==2.33.2 ; python_version >= "3.13" and python_version < "4.0"
pydantic-settings==2.10.1 ; python_version >= "3.13" and python_version < "4.0"
pydantic==2.11.7 ; python_version >= "3.13" and python_version < "4.0"
pygments==2.19.2 ; python_version >= "3.13" and python_version < "4.0"
python-dotenv==1.1.1 ; python_version >= "3.13" and python_version < "4.0"
python-multipart==0.0.20 ; python_version >= "3.13" and python_version < "4.0"
pyyaml==6.0.2 ; python_version >= "3.13" and python_version < "4.0"
redis==6.4.0 ; python_version >= "3.13" and python_version < "4.0"
rich-toolkit==0.14.9 ; python_version >= "3.13" and python_version < "4.0"
rich==14.1.0 ; python_version >= "3.13" and python_version < "4.0"
rignore==0.6.4 ; python_version >= "3.13" and python_version < "4.0"
Expand Down
10 changes: 10 additions & 0 deletions server/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import redis.asyncio as redis

from server.settings import get_settings

settings = get_settings()

# Cria uma conexão global com o Redis
redis_client = redis.Redis(
host=settings.REDIS_HOST, port=settings.REDIS_PORT, decode_responses=True
)
15 changes: 15 additions & 0 deletions server/hasher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import hashlib


def hash_id(user_id: str) -> str:
"""
Gera o hash de uma determinada string.

Args:
user_id (str): Texto para obter o hash

Returns:
str: Hash do texto informado

"""
return hashlib.sha256(user_id.encode('utf-8')).hexdigest()
26 changes: 26 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
"""Confy Server - Servidor web de encaminhamento de mensagens enviadas por aplicativos clientes compatíveis."""

from contextlib import asynccontextmanager

from fastapi import FastAPI

from server.db import redis_client
from server.routers import status, ws

description = """
Este servidor realiza o encaminhamento de mensagens entre usuários conectados
via WebSocket utilizando algum aplicativo cliente compatível com o servidor.
"""


@asynccontextmanager
async def clean_online_users(app: FastAPI):
"""
Gerencia o ciclo de vida da aplicação FastAPI garantindo a limpeza dos usuários online.

Este gerenciador de contexto é executado quando a aplicação inicia e termina.
Ao encerrar o ciclo de vida da aplicação, remove do Redis todos os registros
de usuários que estavam marcados como "online", evitando que conexões antigas
permaneçam ativas indevidamente após um restart do servidor.

Args:
app (FastAPI): Instância da aplicação FastAPI.

Yields:
None: Permite a execução normal da aplicação durante o ciclo de vida.

"""
yield
await redis_client.delete('online_users')


app = FastAPI(
title='Confy Server',
description=description,
Expand All @@ -19,6 +44,7 @@
'url': 'https://github.com/confy-security/server',
'email': 'confy@henriquesebastiao.com',
},
lifespan=clean_online_users,
)

app.include_router(ws.router)
Expand Down
20 changes: 20 additions & 0 deletions server/routers/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

from fastapi import APIRouter, WebSocket, WebSocketDisconnect

from server.db import redis_client
from server.hasher import hash_id
from server.logger import logger

router = APIRouter(prefix='/ws', tags=['WebSocket'])
Expand All @@ -42,12 +44,27 @@ async def websocket_endpoint(websocket: WebSocket, sender_id: str, recipient_id:
recipient_id (str): O ID do destinatário.

"""
sender_id = hash_id(sender_id)
recipient_id = hash_id(recipient_id)

# Aceita a conexão WebSocket do cliente
await websocket.accept()

# Verifica no Redis se o usuário já está conectado
is_online = await redis_client.sismember('online_users', sender_id)
if is_online:
await websocket.send_text(
'system-message: Já há um usuário conectado com o ID que você solicitou.'
)
await websocket.close()
return # encerra a função sem registrar o usuário novamente

# Registra a conexão do remetente como ativa
active_connections[sender_id] = websocket

# Salva no Redis que o usuário está online
await redis_client.sadd('online_users', sender_id)

# Cria um identificador único e imutável para o túnel de comunicação
tunnel_id = frozenset({sender_id, recipient_id})

Expand Down Expand Up @@ -94,6 +111,9 @@ async def websocket_endpoint(websocket: WebSocket, sender_id: str, recipient_id:
if sender_id in active_connections:
del active_connections[sender_id]

# Remove do Redis quando desconectar
await redis_client.srem('online_users', sender_id)

# Se o destinatário ainda estiver conectado, avisa e encerra a conexão dele também
if recipient_id in active_connections:
recipient_connection = active_connections[recipient_id]
Expand Down
15 changes: 15 additions & 0 deletions server/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', extra='ignore')

REDIS_HOST: str
REDIS_PORT: int


@lru_cache
def get_settings():
return Settings()
Empty file removed tests/__init__.py
Empty file.
55 changes: 0 additions & 55 deletions tests/conftest.py

This file was deleted.

23 changes: 0 additions & 23 deletions tests/test_status_endpoint.py

This file was deleted.

Loading