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
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# PastePoint

PastePoint is a secure, feature-rich file-sharing service designed for local networks. It enables users to share files and communicate efficiently through peer-to-peer WebRTC connections. Built with a Rust-based backend using Actix Web and an Angular frontend with SSR support, PastePoint prioritizes security, performance, and usability.
PastePoint is a secure, feature-rich file-sharing service designed for local networks. It enables users to share files and communicate efficiently through peer-to-peer WebRTC connections. Built with a Rust-based backend using Actix Web and an Angular 21 frontend with SSR support, PastePoint prioritizes security, performance, and usability.

## Usage Disclaimer

Expand All @@ -24,7 +24,10 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net

- Establish WebSocket-based local chat between computers on the same network
- List available sessions, create new sessions, or join existing ones
- Multiple rooms within a session — create, list, and switch between rooms
- QR code session sharing — generate a code from one device and scan it from another to join instantly
- Real-time messaging with emoji support and dark/light theme
- Resilient WebSocket signaling with automatic reconnect, heartbeat, and bfcache support

- **File Sharing**:

Expand Down Expand Up @@ -62,10 +65,10 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net

### Server (Rust)

[![Actix](https://img.shields.io/badge/Actix-4.7-blue)](https://actix.rs/)
[![Actix](https://img.shields.io/badge/Actix-4.13-blue)](https://actix.rs/)
[![OpenSSL](https://img.shields.io/badge/OpenSSL-0.10-yellow)](https://www.openssl.org/)

- **Framework**: Actix Web with WebSocket support
- **Framework**: Actix Web with WebSocket support (Rust edition 2024, toolchain 1.93.1)
- **Security**: OpenSSL for TLS termination
- **Utilities**: UUID generation, Serde serialization
- **Rate Limiting**: Actix-governor for request throttling
Expand All @@ -74,22 +77,24 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net

#### Web (Angular)

[![Angular](https://img.shields.io/badge/Angular-19-red)](https://angular.io/)
[![Angular](https://img.shields.io/badge/Angular-21-red)](https://angular.io/)
[![Tailwind](https://img.shields.io/badge/Tailwind-3.4-blue)](https://tailwindcss.com/)
[![Flowbite](https://img.shields.io/badge/Flowbite-3.0-cyan)](https://flowbite.com/)
[![Flowbite](https://img.shields.io/badge/Flowbite-3.1-cyan)](https://flowbite.com/)

- **Rendering**: Server-Side Rendering with Angular Universal
- **Rendering**: Server-Side Rendering with Angular SSR
- **State Management**: RxJS observables
- **Styling**: Tailwind CSS with dark mode
- **I18n**: ngx-translate integration (English, Arabic) (WIP)
- **I18n**: ngx-translate integration (English, Arabic with RTL)
- **WebRTC**: Native WebRTC API for file transfers
- **QR Sharing**: `qrcode` for generation, `jsqr` for camera-based scanning
- **Integrity**: `hash-wasm` for fast file hashing
- **Notifications**: Hot-toast for real-time feedback

### Infrastructure

[![Nginx](https://img.shields.io/badge/Nginx-Reverse_Proxy-green)](https://nginx.org)
[![Docker](https://img.shields.io/badge/Docker-24.0-blue)](https://www.docker.com)
[![Express](https://img.shields.io/badge/Express-4.21-purple)](https://expressjs.com/)
[![Express](https://img.shields.io/badge/Express-4.22-purple)](https://expressjs.com/)

- **Container Orchestration**: Docker Compose with multi-stage builds
- **Reverse Proxy**: Nginx with enhanced security features
Expand All @@ -103,8 +108,7 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net
pastepoint/
├── client/ # Frontend clients
│ ├── web/ # Angular SSR frontend
│ ├── ios/ # iOS client (WIP)
│ └── android/ # Android client (WIP)
│ └── ios/ # iOS client (WIP)
├── server/ # Rust backend with WebSockets
├── nginx/ # Reverse proxy & SSL termination
├── scripts/ # Development & deployment scripts
Expand All @@ -131,7 +135,7 @@ pastepoint/

##### Android:

- Work in progress
- Planned

#### Deployment:

Expand Down Expand Up @@ -162,7 +166,7 @@ pastepoint/

- Docker and Docker Compose
- Node.js (v22.14.0 as specified in `.nvmrc`)
- Rust (stable, specified in `rust-toolchain`)
- Rust 1.93.1 (specified in `rust-toolchain`, edition 2024)

#### Windows-Specific Requirements:

Expand Down Expand Up @@ -205,9 +209,9 @@ pastepoint/
- Frontend:
- Localhost: [https://localhost](https://localhost)
- Local Network: `https://<your-local-ip>`
- Server API:
- Localhost: [https://localhost:9000](https://localhost:9000)
- Local Network: `https://<your-local-ip>:9000`
- WebSocket signaling:
- Localhost: `wss://localhost:9000/ws`
- Local Network: `wss://<your-local-ip>:9000/ws`

## Contributing

Expand Down
4 changes: 2 additions & 2 deletions client/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.16.1",
"version": "0.16.2",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
1 change: 1 addition & 0 deletions client/web/src/app/core/i18n/localizations/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"CANNOT_CONNECT_TO_USER": "تعذّر الاتصال بالمستخدم {{userName}}. قد لا يكون متصلاً.",
"CONNECTION_CLOSED_WITH_USER": "تم إغلاق الاتصال مع المستخدم {{userName}}.",
"SERVER_CONNECTION_FAILED": "فشل الاتصال بالخادم. يرجى التحقق من اتصال الإنترنت.",
"SESSION_JOIN_FAILED": "تعذر الانضمام إلى الجلسة. قد يكون الرمز غير صالح أو منتهي الصلاحية، أو أن الخادم غير متاح.",
"SERVER_CONNECTION_LOST_PERMANENTLY": "تم فقد الاتصال بالخادم. يرجى تحديث الصفحة.",
"RECONNECTING_TO_SERVER": "جارٍ إعادة الاتصال بالخادم...",
"CANCELLED": "تم الإلغاء",
Expand Down
1 change: 1 addition & 0 deletions client/web/src/app/core/i18n/localizations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"CANNOT_CONNECT_TO_USER": "Cannot connect to {{userName}}. They may be offline.",
"CONNECTION_CLOSED_WITH_USER": "Connection with {{userName}} was closed.",
"SERVER_CONNECTION_FAILED": "Failed to connect to server. Please check your internet connection.",
"SESSION_JOIN_FAILED": "Couldn't join the session. The code may be invalid or expired, or the server may be unreachable.",
"SERVER_CONNECTION_LOST_PERMANENTLY": "Lost connection to server. Please refresh the page.",
"RECONNECTING_TO_SERVER": "Reconnecting to server...",
"CANCELLED": "Canceled",
Expand Down
1 change: 1 addition & 0 deletions client/web/src/app/core/interfaces/webrtc.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export interface IWebRTCService {

initiateConnection(targetUser: string): void;
sendData(data: DataChannelMessage, targetUser: string): void;
closeConnection(targetUser: string): void;
closeAllConnections(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,26 @@ export class WebRTCSignalingService {
}
}

/**
* Stronger than closePeerConnection — also cancels pending retries and
* queued requests. Use when the peer is gone for good (left the room).
*/
public closeConnection(targetUser: string): void {
this.closePeerConnection(targetUser, true);

this.connectionLocks.delete(targetUser);
this.reconnectAttempts.delete(targetUser);
this.lastSequences.delete(targetUser);

const reconnectionTimeout = this.reconnectionTimeouts.get(targetUser);
if (reconnectionTimeout) {
clearTimeout(reconnectionTimeout);
this.reconnectionTimeouts.delete(targetUser);
}

this.communicationService.deleteMessageQueue(targetUser);
}

/**
* Closes a peer connection with the target user
* @param targetUser The user to disconnect from
Expand All @@ -246,8 +266,8 @@ export class WebRTCSignalingService {
public closePeerConnection(targetUser: string, force = false) {
const peerConnection = this.peerConnections.get(targetUser);
if (peerConnection) {
peerConnection.close();
this.peerConnections.delete(targetUser);
peerConnection.close();
}

// Clear connection request timeout
Expand Down Expand Up @@ -286,7 +306,9 @@ export class WebRTCSignalingService {
peerConnection.close();
});
this.peerConnections.clear();
this.connectionLocks.clear();
this.reconnectAttempts.clear();
this.lastSequences.clear();
this.candidateQueues.clear();
this.collectedCandidates.clear();

Expand Down Expand Up @@ -923,7 +945,6 @@ export class WebRTCSignalingService {
this.logger.info('reconnect', `Reconnecting WebRTC with ${targetUser}...`);

this.closePeerConnection(targetUser, true);
this.reconnectAttempts.set(targetUser, 0);
this.initiateConnection(targetUser);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export class WebRTCService implements IWebRTCService {
return this.communicationService.isConnected(targetUser);
}

public closeConnection(targetUser: string): void {
this.signalingService.closeConnection(targetUser);
}

/**
* Closes all connections
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,21 @@ export class WebSocketConnectionService implements OnDestroy {
const socket = new WebSocket(wsUri);
this.socket = socket;

// Settle once — onclose may fire without onopen/onerror, e.g. when a
// concurrent disconnect replaces this.socket and the stale-event guards
// below skip the natural reject path.
let settled = false;
const settleResolve = () => {
if (settled) return;
settled = true;
resolve();
};
const settleReject = (err: unknown) => {
if (settled) return;
settled = true;
reject(err);
};

socket.onopen = () => {
if (socket !== this.socket) return;
this.logger.info('connect', 'WebSocket connected');
Expand All @@ -188,7 +203,7 @@ export class WebSocketConnectionService implements OnDestroy {
this.reconnectDelay = 1000;
this.isConnecting = false;
this.startKeepAlive();
resolve();
settleResolve();
};

socket.onmessage = (ev) => {
Expand Down Expand Up @@ -219,6 +234,10 @@ export class WebSocketConnectionService implements OnDestroy {
};

socket.onclose = (event) => {
// Must run before the stale-event guard — the promise belongs to
// this socket, even if it's already been replaced.
settleReject(new Error(`WebSocket closed before opening (code ${event.code})`));

// Ignore stale close events from a socket that has already been replaced.
// Without this guard, a late-firing onclose from a previous socket would
// null-out the current `this.socket` and trigger a phantom reconnect,
Expand Down Expand Up @@ -265,10 +284,7 @@ export class WebSocketConnectionService implements OnDestroy {
this.isConnecting = false;
this.stopKeepAlive();
this.socket = undefined;
if (this.reconnectAttempts === 0 && !this.manualDisconnect) {
this.toaster.error(this.translate.instant('SERVER_CONNECTION_FAILED'));
}
reject(error);
settleReject(error);
};
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export class RoomService implements IRoomService {
this.wsService.send('[UserCommand] /list');
}

/**
* Resets singleton state on session transition so BehaviorSubjects don't
* replay the previous session's members/rooms into the new view.
*/
public reset(): void {
this.ngZone.run(() => {
this.rooms$.next([]);
this.members$.next([]);
this.currentRoom = 'main';
});
}

public joinRoom(room: string): void {
const sanitizedRoom = room
.replace(/[^a-zA-Z0-9\-_ ]/g, '')
Expand Down
3 changes: 1 addition & 2 deletions client/web/src/app/features/chat/chat.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,8 @@
</div>
<div class="flex items-center gap-4">
<a
href="/privacy"
routerLink="/privacy"
Comment thread
SloMR marked this conversation as resolved.
title="Privacy and Terms"
target="_self"
rel="noopener noreferrer nofollow"
class="inline-flex items-center gap-2 text-brand dark:text-brandDark"
>
Expand Down
Loading
Loading