Packages
phoenix_kit
1.7.34
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/sync/web/socket_plug.ex
defmodule PhoenixKit.Modules.Sync.Web.SocketPlug do
@moduledoc """
Plug for handling DB Sync WebSocket connections.
This plug handles the HTTP upgrade to WebSocket and validates
the connection code or auth token before handing off to SyncWebsock.
## Authentication Methods
Supports two authentication methods:
1. **Session Code** (ephemeral) - For manual one-time transfers
- Query param: `?code=ABC12345`
- Session is tied to LiveView process
2. **Connection Token** (permanent) - For persistent connections
- Query param: `?token=xyz123...`
- Validated against database, subject to access controls
## Usage
In your endpoint:
plug PhoenixKit.Modules.Sync.Web.SocketPlug
Or mount at a specific path in router (done automatically by phoenix_kit_socket macro).
"""
@behaviour Plug
require Logger
alias PhoenixKit.Modules.Sync
alias PhoenixKit.Modules.Sync.Connections
@impl Plug
def init(opts), do: opts
@impl Plug
def call(conn, _opts) do
# When used with forward in router, the path is stripped to "/"
# When used directly in endpoint (deprecated), check if path ends with /sync/websocket
cond do
conn.request_path == "/" ->
# Forwarded from router - handle the request
handle_websocket_request(conn)
String.ends_with?(conn.request_path, "/sync/websocket") ->
# Direct endpoint use (deprecated) - still handle for backwards compatibility
handle_websocket_request(conn)
true ->
# Not a sync websocket request - pass through
conn
end
end
defp handle_websocket_request(conn) do
# Check if this is a WebSocket upgrade request
if websocket_request?(conn) do
if Sync.enabled?() do
conn = Plug.Conn.fetch_query_params(conn)
code = conn.query_params["code"]
token = conn.query_params["token"]
cond do
# Permanent connection token authentication
token != nil ->
validate_token_and_upgrade(conn, token)
# Ephemeral session code authentication
code != nil ->
validate_code_and_upgrade(conn, code)
# No authentication provided
true ->
Logger.warning("Sync: Connection attempt without code or token")
send_forbidden(conn, "Missing authentication")
end
else
Logger.warning("Sync: Connection attempt but module is disabled")
send_forbidden(conn, "Module disabled")
end
else
send_bad_request(conn, "Expected WebSocket upgrade")
end
end
defp websocket_request?(conn) do
upgrade_header =
Plug.Conn.get_req_header(conn, "upgrade")
|> List.first()
|> Kernel.||("")
|> String.downcase()
upgrade_header == "websocket"
end
# ===========================================
# SESSION CODE AUTHENTICATION (EPHEMERAL)
# ===========================================
defp validate_code_and_upgrade(conn, code) do
case Sync.validate_code(code) do
{:ok, session} ->
Logger.info("Sync: Receiver connecting with code #{code}")
# Capture connection metadata
connection_info = extract_connection_info(conn)
conn =
WebSockAdapter.upgrade(
conn,
PhoenixKitWeb.SyncWebsock,
[
auth_type: :session,
code: code,
session: session,
connection_info: connection_info
],
timeout: 60_000
)
Plug.Conn.halt(conn)
{:error, :invalid_code} ->
Logger.warning("Sync: Invalid code attempt: #{code}")
send_forbidden(conn, "Invalid code")
{:error, :already_used} ->
Logger.warning("Sync: Code already used: #{code}")
send_forbidden(conn, "Code already used")
end
end
# ===========================================
# CONNECTION TOKEN AUTHENTICATION (PERMANENT)
# ===========================================
defp validate_token_and_upgrade(conn, token) do
# Extract client IP for validation
client_ip = get_remote_ip(conn)
case Connections.validate_connection(token, client_ip) do
{:ok, db_connection} ->
# Check for download password if required
password = conn.query_params["password"]
case Connections.validate_download_password(db_connection, password) do
:ok ->
Logger.info("Sync: Token connection validated for #{db_connection.name}")
# Update last connected timestamp
Connections.touch_connected(db_connection)
# Capture connection metadata
connection_info = extract_connection_info(conn)
conn =
WebSockAdapter.upgrade(
conn,
PhoenixKitWeb.SyncWebsock,
[
auth_type: :connection,
connection: db_connection,
connection_info: connection_info
],
timeout: 60_000
)
Plug.Conn.halt(conn)
{:error, :invalid_password} ->
Logger.warning("Sync: Invalid download password for connection #{db_connection.id}")
send_forbidden(conn, "Invalid password")
end
{:error, :invalid_token} ->
Logger.warning("Sync: Invalid token attempt")
send_forbidden(conn, "Invalid token")
{:error, :connection_not_active} ->
Logger.warning("Sync: Token for inactive connection")
send_forbidden(conn, "Connection not active")
{:error, :connection_expired} ->
Logger.warning("Sync: Token for expired connection")
send_forbidden(conn, "Connection expired")
{:error, :download_limit_reached} ->
Logger.warning("Sync: Download limit reached")
send_forbidden(conn, "Download limit reached")
{:error, :record_limit_reached} ->
Logger.warning("Sync: Record limit reached")
send_forbidden(conn, "Record limit reached")
{:error, :ip_not_allowed} ->
Logger.warning("Sync: IP not in whitelist: #{client_ip}")
send_forbidden(conn, "IP not allowed")
{:error, :outside_allowed_hours} ->
Logger.warning("Sync: Connection outside allowed hours")
send_forbidden(conn, "Outside allowed hours")
end
end
defp extract_connection_info(conn) do
# Get remote IP - check for forwarded headers first (for proxies)
remote_ip = get_remote_ip(conn)
# Get user agent
user_agent =
Plug.Conn.get_req_header(conn, "user-agent")
|> List.first()
# Get origin/referer
origin =
Plug.Conn.get_req_header(conn, "origin")
|> List.first()
referer =
Plug.Conn.get_req_header(conn, "referer")
|> List.first()
# Get host info
host = conn.host
port = conn.port
scheme = if conn.scheme == :https, do: "https", else: "http"
# Get WebSocket protocol version
ws_version =
Plug.Conn.get_req_header(conn, "sec-websocket-version")
|> List.first()
# Get accept-language for locale info
accept_language =
Plug.Conn.get_req_header(conn, "accept-language")
|> List.first()
%{
remote_ip: remote_ip,
user_agent: user_agent,
origin: origin,
referer: referer,
host: host,
port: port,
scheme: scheme,
request_path: conn.request_path,
query_string: conn.query_string,
websocket_version: ws_version,
accept_language: accept_language,
connected_at: DateTime.utc_now()
}
end
defp get_remote_ip(conn) do
# Check X-Forwarded-For first (for load balancers/proxies)
forwarded_for =
Plug.Conn.get_req_header(conn, "x-forwarded-for")
|> List.first()
if forwarded_for do
# Take the first IP in the chain (original client)
forwarded_for
|> String.split(",")
|> List.first()
|> String.trim()
else
# Fall back to direct connection IP
conn.remote_ip
|> :inet.ntoa()
|> to_string()
end
end
defp send_forbidden(conn, message) do
conn
|> Plug.Conn.put_resp_content_type("text/plain")
|> Plug.Conn.send_resp(403, message)
|> Plug.Conn.halt()
end
defp send_bad_request(conn, message) do
conn
|> Plug.Conn.put_resp_content_type("text/plain")
|> Plug.Conn.send_resp(400, message)
|> Plug.Conn.halt()
end
end