Skip to content

manmohak07/echoit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

23 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Echoit

Secure, ephemeral peer-to-peer file sharing. no accounts, no cloud storage, no permanent copies.

A sender uploads a file and gets a port-based invite code plus a one-time PIN. The recipient enters both to download the file. The file is automatically destroyed after 60 seconds or after 2 failed PIN attempts, whichever comes first.


Features

Feature Details
Drag & drop upload Click or drop any file to start sharing
Invite code A random ephemeral port (49152–65535) acts as the share link
PIN authentication A random 4-digit PIN is generated per session; recipient needs both code + PIN
Session timer 60-second countdown starts on upload; file auto-deletes at zero
Colour-coded timer Green > 30 s β†’ Amber 10–30 s β†’ Red < 10 s
PIN retry limiting Max 2 wrong attempts before the session is permanently terminated
Copy to clipboard One-click copy for both the invite code and PIN
Auto file cleanup Temp file + TCP socket closed on expiry or lockout
Docker support Single docker-compose up spins up both services
One-command startup start.sh / start.bat builds and launches everything locally

Architecture

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚                        Browser  :3000                                 β”‚
  β”‚                                                                       β”‚
  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
  β”‚   β”‚  FileUpload.tsx  β”‚    β”‚ FileDownload.tsx  β”‚   β”‚InviteCode.tsx β”‚   β”‚
  β”‚   β”‚  (drag & drop)   β”‚    β”‚  (port + PIN form)β”‚   β”‚(timer + copy) β”‚   β”‚
  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ POST /api/upload      β”‚ GET /api/download/:port?pin=
               β–Ό                       β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚               Next.js API Proxy  (next.config.js)                    β”‚
  β”‚          /api/*  ──────────────►  http://localhost:8080              β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚ HTTP  :8080
                                        β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚                    Java Backend  :8080                               β”‚
  β”‚                                                                      β”‚
  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
  β”‚   β”‚   UploadHandler    β”‚      β”‚        DownloadHandler          β”‚    β”‚
  β”‚   β”‚  POST /upload      β”‚      β”‚   GET /download/:port?pin=      β”‚    β”‚
  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
  β”‚             β”‚                                 β”‚                      β”‚
  β”‚             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
  β”‚                            β–Ό                                         β”‚
  β”‚             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”‚
  β”‚             β”‚          FileSharer          β”‚                         β”‚
  β”‚             β”‚  offerFile()  validatePin()  β”‚                         β”‚
  β”‚             β”‚  startFileServer()           β”‚                         β”‚
  β”‚             β”‚  terminateSession()          β”‚                         β”‚
  β”‚             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β”‚
  β”‚                            β”‚                                         β”‚
  β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
  β”‚          β–Ό                                    β–Ό                      β”‚
  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
  β”‚  β”‚      FileSession      β”‚      β”‚  TCP ServerSocket :49152–65535β”‚    β”‚
  β”‚  β”‚  fileName             β”‚      β”‚  (one per active session)     β”‚    β”‚
  β”‚  β”‚  port                 β”‚      β”‚  FileSenderHandler (thread)   β”‚    β”‚
  β”‚  β”‚  pin                  β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
  β”‚  β”‚  retryCount (max 2)   β”‚                                           β”‚
  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                           β”‚
  β”‚                                                                      β”‚
  β”‚         ScheduledExecutorService  ──►  cleanUpSession() @ +60 s      β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key design choice: The HTTP server is purely the control plane. Actual file bytes travel over a raw TCP socket on an ephemeral port. The Java backend opens a Socket("127.0.0.1", port) to its own TCP server and pipes the bytes through the HTTP response and the browser never touches the TCP layer.


Data Flow

Upload Flow

  User selects / drops a file
          β”‚
          β–Ό
  FileUpload.tsx
          β”‚
          β”‚  POST /api/upload  (multipart/form-data)
          β–Ό
  Next.js proxy  ──────────────────────────►  Java UploadHandler :8080
                                                      β”‚
                                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                          β”‚  Parse multipart body  β”‚
                                          β”‚  (custom MultiParser)  β”‚
                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                          β”‚  Save to OS temp dir   β”‚
                                          β”‚  /tmp/echoit-uploads/  β”‚
                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                          β”‚  FileSharer            β”‚
                                          β”‚  Β· pick random port    β”‚
                                          β”‚    (49152–65535)       β”‚
                                          β”‚  Β· generate 4-digit PINβ”‚
                                          β”‚  Β· create FileSession  β”‚
                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                       β–Ό                             β–Ό
                              Start TCP ServerSocket       Schedule 60 s cleanup
                              on chosen port (thread)      (ScheduledExecutorService)
                                       β”‚
                                       β–Ό
                              Wait for one connection
                              (ServerSocket.accept())
                                        β”‚
                                        β–Ό
                                JSON { port, pin }
                                        β”‚
                                        β–Ό
                                InviteCode.tsx
                                Β· Display invite code + PIN
                                Β· Start 60 s countdown
                                Β· Copy-to-clipboard buttons

Download Flow

  Receiver enters invite code (port) + PIN
          β”‚
          β–Ό
  FileDownload.tsx
          β”‚
          β”‚  GET /api/download/:port?pin=XXXX
          β–Ό
  Next.js proxy  ──────────────────────────►  Java DownloadHandler :8080
                                                      β”‚
                                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                          β”‚  Parse port from path  β”‚
                                          β”‚  Parse pin from query  β”‚
                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                          β”‚  FileSharer            β”‚
                                          β”‚  .validatePin()        β”‚
                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                    β”‚                                 β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚   NOT_FOUND        β”‚           β”‚   WRONG_PIN            β”‚
          β”‚   β†’ 404            β”‚           β”‚   retryCount--         β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚   β†’ 401 + retriesLeft  β”‚
                                           β”‚                        β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   retryCount == 0?     β”‚
                    β”‚                      β”‚   β†’ terminateSession() β”‚
                    β”‚                      β”‚   β†’ 403 LOCKED_OUT     β”‚
                    β”‚                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚   CORRECT          β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β”‚  open TCP Socket β†’ 127.0.0.1:<port>
                    β–Ό
          FileSenderHandler (Java thread)
          Β· write  "Filename: <name>\n"
          Β· stream file bytes
                    β”‚
                    β–Ό
          DownloadHandler reads TCP stream
          Β· parse filename from header line
          Β· pipe bytes to HTTP response
            Content-Disposition: attachment; filename="<name>"
                    β”‚
                    β–Ό
          Browser saves file to disk

Session Lifecycle

  File uploaded
       β”‚
       β–Ό
  FileSession created
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ fileName  β†’ /tmp/...    β”‚
  β”‚ port      β†’ 54321       β”‚
  β”‚ pin       β†’ 7382        β”‚
  β”‚ retryCount β†’ 2          β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚                                β”‚
       β–Ό                                β–Ό
  Wrong PIN entered               60 s timer fires
  retryCount = 1                  ScheduledExecutor
       β”‚                                β”‚
       β–Ό                                β”‚
  Wrong PIN again                       β”‚
  retryCount = 0                        β”‚
       β”‚                                β”‚
       β–Ό                                β–Ό
  terminateSession()  ◄─────────  cleanUpSession()
       β”‚
       β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Β· ServerSocket.close()  β”‚
  β”‚ Β· File.delete()         β”‚
  β”‚ Β· Remove from HashMap   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Prerequisites

Requirement Version
Node.js + npm 18+
Java JDK 17+
Maven 3.6+
Docker + Compose (optional, for containerised deployment)

Getting Started

Option 1 β€” On a VPS (recommended)

1. git clone https://github.com/manmohak07/echoit.git
2. cd echoit
3. chmod +x vps-setup.sh  
4. ./vps-setup.sh   

The vps-setup.sh script will:

  • Install Java 17, Node.js, Maven, Nginx, and PM2

  • Build both frontend and backend

  • Configure Nginx reverse proxy

  • Start services with PM2

  • Set up auto-restart on boot


Option 2 β€” Local

1. git clone https://github.com/manmohak07/echoit.git
2. cd echoit
3. mvn clean package
4. java -jar target/echoit-1.0-SNAPSHOT.jar
5. cd ui
6. npm install
7. npm run dev
8. Access the application
9. Frontend: http://localhost:3000
10. Backend: http://localhost:8080

Option 3 β€” Docker

docker-compose up --build
  • Backend on :8080, frontend on :3000
  • The BACKEND_URL env var in the compose file routes the Next.js proxy to the backend container automatically.

How to Use

Sharing a file

  1. Open the Share a File tab.
  2. Drag & drop a file or click to browse.
  3. Once the backend responds, you'll see:
    • an invite code (the ephemeral port number)
    • a 4-digit PIN
  4. Share both values with your recipient β€” they need each one to download.
  5. The session is live for 60 seconds. The file is gone when the timer hits zero.

Receiving a file

  1. Open the Receive a File tab.
  2. Enter the invite code and the PIN.
  3. Click Download File β€” the file arrives in your browser with its original name.
  4. You get 2 attempts to enter the correct PIN. On the third failure the session is permanently destroyed.

Project Structure

echoit/
β”œβ”€β”€ src/main/java/echoit/
β”‚   β”œβ”€β”€ App.java                        ← entry point, starts HTTP server on :8080
β”‚   β”œβ”€β”€ controller/
β”‚   β”‚   β”œβ”€β”€ FileController.java         ← HTTP server, UploadHandler, MultiParser, CORS
β”‚   β”‚   └── DownloadHandler.java        ← PIN validation, TCPβ†’HTTP byte bridging
β”‚   β”œβ”€β”€ service/
β”‚   β”‚   └── FileSharer.java             ← session registry, TCP file server, PIN logic
β”‚   └── util/
β”‚       β”œβ”€β”€ FileSession.java            ← session model (fileName, port, pin, retryCount)
β”‚       └── UploadUtils.java            ← random port + PIN generators
β”‚
β”œβ”€β”€ ui/                                 ← Next.js frontend
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ app/                        ← App Router pages & global styles
β”‚   β”‚   └── components/
β”‚   β”‚       β”œβ”€β”€ FileUpload.tsx          ← drag-and-drop upload zone
β”‚   β”‚       β”œβ”€β”€ FileDownload.tsx        ← invite code + PIN download form
β”‚   β”‚       └── InviteCode.tsx          ← countdown timer, code & PIN display
β”‚   └── next.config.js                  ← API proxy rewrites + BACKEND_URL config
β”‚
β”œβ”€β”€ Dockerfile.backend
β”œβ”€β”€ Dockerfile.frontend
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ start.sh                            ← one-command launcher (Linux/macOS)
β”œβ”€β”€ start.bat                           ← one-command launcher (Windows)
└── pom.xml

Tech Stack

Layer Technology
Frontend Next.js 14, React 18, TypeScript, Tailwind CSS
HTTP backend Java 17, com.sun.net.httpserver.HttpServer
File transfer Raw TCP sockets (java.net.ServerSocket)
Build Maven (backend), npm (frontend)
Containerisation Docker, Docker Compose, Nginx

About

This is a Spring Boot based application to share your files less than 10 KB to anyone, anywhere.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors