### ---------------------------------------------------------------------- ### ### Copyright (c) 2013 - 2020 Jahred Love and Xirsys LLC ### ### All rights reserved. ### ### XTurn is licensed by Xirsys 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. ### ### See LICENSE for the full license text. ### ### ---------------------------------------------------------------------- defmodule Xirsys.Sockets.Client do @moduledoc """ TCP protocol socket client """ use GenServer require Logger alias Xirsys.Sockets.{Conn, Socket} ##### # External API @type callback() :: (pid(), any(), binary(), binary() -> :ok) @doc """ Standard OTP module startup """ @spec start_link(pid(), callback(), boolean()) :: :ok def start_link(socket, callback, ssl) do GenServer.start_link(__MODULE__, [socket, callback, ssl]) end def create(socket, callback, ssl) do Xirsys.Sockets.SockSupervisor.start_child(socket, callback, ssl) end def init([socket, callback, ssl]) do Logger.debug("Client init") {:ok, %{ callback: callback, accepted: false, list_socket: socket, cli_socket: nil, addr: nil, buffer: <<>>, ssl: ssl, cache: %{} }, 0} end @doc """ Asynchronous socket response handler """ def handle_cast({msg, ip, port}, %{cli_socket: socket} = state) do # Select proper client Logger.debug( "Dispatching TCP to #{inspect(ip)}:#{inspect(port)} | #{inspect(byte_size(msg))} bytes" ) Socket.send(socket, msg) {:noreply, state} end def handle_cast(:stop, state), do: {:stop, :normal, state} def handle_cast(other, state) do Logger.debug("TCP client: strange cast: #{inspect(other)}") {:noreply, state} end def handle_call(other, _from, state) do Logger.debug("TCP client: strange call: #{inspect(other)}") {:noreply, state} end def handle_info(:timeout, %{list_socket: list_socket, callback: cb} = state) do Logger.debug("handle_info timeout #{inspect(cb)}") with {:ok, cli_socket} <- Socket.handshake(list_socket), {:ok, client_ip_port} <- Socket.peername(cli_socket), {:ok, {_, sport}} <- Socket.sockname(cli_socket) do Logger.debug("#{inspect(list_socket)}") create(list_socket, cb, false) Socket.set_sockopt(list_socket, cli_socket) Socket.setopts(cli_socket) Logger.debug("returning from timeout") {:noreply, %{ state | accepted: true, cli_socket: cli_socket, addr: {client_ip_port, {Socket.server_ip(), sport}} }} end end @doc """ Message handler to update cache """ def handle_info({:cache, cache}, state) do {:noreply, %{state | :cache => cache}} end def handle_info( {_, _client, data}, %{cli_socket: socket, addr: {{fip, fport}, {_, tport}}, buffer: buffer, cache: cache} = state ) do Logger.debug("handle_info tcp") with {:ok, ip_port} <- Socket.peername(socket) do Logger.debug("TCP called from #{inspect(ip_port)} with #{inspect(byte_size(data))} BYTES") buffer = case state.callback.process_buffer(<>) do {nil, buffer} -> buffer {packet, buffer} -> conn = %Conn{ message: packet, listener: self(), client_socket: socket, client_ip: fip, client_port: fport, server_ip: Socket.server_ip(), server_port: tport, cache: cache } state.callback.dispatch(conn) Socket.send_to_client_hooks(conn) buffer end Socket.setopts(socket) {:noreply, %{state | :buffer => buffer}} end end def handle_info({:ssl_closed, client}, state) do Logger.debug("Client #{inspect(client)} closed connection") {:stop, :normal, state} end def handle_info({:tcp_closed, client}, state) do Logger.debug("Client #{inspect(client)} closed connection") {:stop, :normal, state} end def handle_info(info, state) do Logger.debug("TCP client: strange info: #{inspect(info)}") {:noreply, state} end def terminate( reason, %{cli_socket: socket, list_socket: list_socket, callback: cb, accepted: false, ssl: ssl} = _state ) do create(list_socket, cb, ssl) Socket.close(socket) Logger.debug("TCP client closed: #{inspect(reason)}") :ok end def terminate(reason, %{cli_socket: socket} = _state) do Socket.close(socket) Logger.debug("TCP client closed: #{inspect(reason)}") :ok end def code_change(_old_vsn, state, _extra) do {:ok, state} end end