Packages
vertexcache_sdk
1.0.0
Elixir SDK for VertexCache — Secure, Fast, Multi-Index Caching Client
Current section
Files
Jump to
Current section
Files
sdk/comm/client_connector.ex
# ------------------------------------------------------------------------------
# Copyright 2025 to Present, Jason Lam - VertexCache (https://github.com/vertexcache)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ------------------------------------------------------------------------------
defmodule VertexCacheSdk.Comm.ClientConnector do
@moduledoc """
Primary SDK transport layer for VertexCache.
Handles TLS, IDENT handshake, framing, and encryption.
"""
alias VertexCacheSdk.Comm.{SocketHelper, MessageCodec}
alias VertexCacheSdk.Model.{ClientOption, EncryptionMode, VertexCacheSdkException}
alias VertexCacheSdk.Comm.{GcmCryptoHelper, KeyParserHelper}
@type socket :: port() | :ssl.sslsocket()
@type state :: %{socket: socket(), opts: ClientOption.t()}
@spec new(ClientOption.t()) :: state()
def new(%ClientOption{} = opts) do
%{socket: nil, opts: opts}
end
@spec connect(map()) :: {:ok, state()} | {:error, VertexCacheSdkException.t()}
def connect(%{opts: %ClientOption{} = opts}) do
try do
{:ok, sock} =
if opts.enable_tls_encryption do
SocketHelper.create_secure_socket(opts)
else
SocketHelper.create_socket_non_tls(opts)
end
ident = ClientOption.build_ident_command(opts)
payload = encrypt_if_enabled(ident, opts)
send_payload(sock, payload, opts)
response = read_response(sock, opts)
unless String.starts_with?(response, "+OK") do
raise VertexCacheSdkException, message: "Authorization failed: #{response}"
end
{:ok, %{socket: sock, opts: opts}}
rescue
e in VertexCacheSdkException -> {:error, e}
e -> {:error, %VertexCacheSdkException{message: Exception.message(e)}}
end
end
@spec send(state(), String.t()) :: {:ok, String.t()} | {:error, VertexCacheSdkException.t()}
def send(%{socket: sock, opts: opts} = _state, message) do
try do
payload = encrypt_if_enabled(message, opts)
send_payload(sock, payload, opts)
response = read_response(sock, opts)
{:ok, response}
rescue
e in VertexCacheSdkException -> {:error, e}
_ -> {:error, %VertexCacheSdkException{message: "Unexpected failure during send"}}
end
end
@spec is_connected(state()) :: boolean()
def is_connected(%{socket: nil}), do: false
def is_connected(%{socket: sock}) do
raw_sock =
case sock do
{:sslsocket, {_, port, _, _}, _} -> port
_ -> sock
end
case :inet.peername(raw_sock) do
{:ok, _} -> true
_ -> false
end
end
@spec close(state()) :: :ok
def close(%{socket: nil}), do: :ok
def close(%{socket: sock}) do
if match?({:sslsocket, _, _}, sock) do
:ssl.close(sock)
else
:gen_tcp.close(sock)
end
:ok
end
# ------------------------
# Private helpers below
# ------------------------
defp encrypt_if_enabled(payload, %ClientOption{encryption_mode: :none}) when is_binary(payload),
do: payload
defp encrypt_if_enabled(payload, %ClientOption{encryption_mode: :symmetric, shared_encryption_key: key}) do
decoded = KeyParserHelper.config_shared_key_if_enabled(key)
GcmCryptoHelper.encrypt(payload, decoded)
end
defp encrypt_if_enabled(payload, %ClientOption{encryption_mode: :asymmetric, public_key: pem}) do
der = KeyParserHelper.config_public_key_if_enabled(pem)
entry = {:SubjectPublicKeyInfo, der, :not_encrypted}
rsa_key = :public_key.pem_entry_decode(entry)
:public_key.encrypt_public(payload, rsa_key)
end
defp send_payload(sock, payload, opts) do
framed = MessageCodec.write_framed_message(payload, opts)
if match?({:sslsocket, _, _}, sock) do
:ssl.send(sock, framed)
else
:gen_tcp.send(sock, framed)
end
:ok
end
defp read_response(sock, opts) do
raw =
if match?({:sslsocket, _, _}, sock) do
:ssl.recv(sock, 0, 5000)
else
:gen_tcp.recv(sock, 0, 5000)
end
case raw do
{:ok, data} ->
case MessageCodec.read_framed_message(data) do
{:ok, payload, _} -> payload
:error -> raise VertexCacheSdkException, message: "Incomplete response framing"
{:error, reason} -> raise VertexCacheSdkException, message: "Framing error: #{inspect(reason)}"
end
{:error, reason} ->
raise VertexCacheSdkException, message: "Socket read failed: #{inspect(reason)}"
end
end
end