unit astamiclient; {< Simple TCP client that connects to the Asterisk AMI server and receives events and data for distribution to amiproxy clients.} { This file is part of AMIProxyPal. AMIProxyPal is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Foobar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Foobar. If not, see . } {$mode objfpc}{$H+} interface uses Classes, blcksock, SysUtils, strutils, clientconnection, thread_communicator ; type // ----------------------------------------------------------------- // objects // ----------------------------------------------------------------- // forward declaration TOutgoingMinder = class; {: Threaded Object for communication between proxy and Asterisk AMI server.} TAstAMIClient = class(TThread) FConnected: boolean; FHost: string; FPort: string; FThreadCom: TThreadCommunicator; FAMIPassword: string; FAMIUserName: string; private FQueue: TAstMessageQueue; FSock: TTCPBlockSocket; FClientList: TClientConnectionList; procedure SetAMIPassword( const AValue: string) ; procedure SetAMIUserName( const AValue: string) ; procedure SetConnected( const AValue: boolean) ; procedure SetHost( const AValue: string) ; procedure SetPort( const AValue: string) ; protected // ---> methods procedure AddToOutgoing(const AEvent: string); virtual; procedure ProcessIncomingRequests; virtual; procedure CheckForMsg; virtual; public // ---> methods procedure Connect; virtual; procedure Disconnect; virtual; function Login(AHost, APort, AUserName, APassword: string): boolean; virtual; function Login: boolean; overload; virtual; // ---> properties property Connected: boolean read FConnected write SetConnected; property Port: string read FPort write SetPort; property Host: string read FHost write SetHost; property AMIUserName: string read FAMIUserName write SetAMIUserName; property AMIPassword: string read FAMIPassword write SetAMIPassword; // ---> overridden procedure Execute; override; constructor Create(AClientList: TClientConnectionList; const AHost, APort: string; AThreadCom, AHelperThreadCom: TThreadCommunicator; AQueue: TAstMessageQueue); destructor Destroy; override; end; {: Helper thread to monitor local Queue. } TOutgoingMinder = class(TThread) private FClientList: TClientConnectionList; FQueue: TAstMessageQueue; FThreadCom: TThreadCommunicator; protected procedure DistributeEvent; virtual; {: Distributes an event packet to clients. } procedure FeedTheClientsEvents(const APacket: string); virtual; {: Distributes a response to the correct client. } procedure GiveClientResponse(var APacket: string); {: Getincoming Requests from clients. } procedure AddRequestsToQueue; public // ---> overridden procedure Execute; override; constructor Create(AClientList: TClientConnectionList; AQueue: TAstMessageQueue; AThreadCom: TThreadCommunicator); overload; destructor Destroy; override; end; implementation uses astevent_utils ; procedure TAstAMIClient.SetConnected( const AValue: boolean) ; begin FConnected := AValue; end; procedure TAstAMIClient.SetAMIPassword( const AValue: string) ; begin if FAMIPassword= AValue then exit; FAMIPassword:= AValue; end; procedure TAstAMIClient.SetAMIUserName( const AValue: string) ; begin if FAMIUserName= AValue then exit; FAMIUserName:= AValue; end; procedure TAstAMIClient.SetHost( const AValue: string) ; begin if FHost= AValue then exit; FHost:= AValue; end; procedure TAstAMIClient.SetPort( const AValue: string) ; begin if FPort= AValue then exit; FPort:= AValue; end; procedure TAstAMIClient.AddToOutgoing(const AEvent: string); begin FQueue.Lock; try FQueue.Outgoing.Add(AEvent); finally FQueue.Unlock; end; end; procedure TAstAMIClient.ProcessIncomingRequests; var i: integer; sPacket: string; begin // loop through and send to Asterisk AMI, any pending requests from clients. FQueue.Lock; try for i := FQueue.Incoming.Count -1 downto 0 do begin sPacket := FQueue.Incoming[i]; FSock.SendString(sPacket + #13#10#13#10); end; // clear incoming queue FQueue.Incoming.Clear; finally FQueue.Unlock; end; end; procedure TAstAMIClient.CheckForMsg; var sMsg: string; begin FThreadCom.Lock; try sMsg := FThreadCom.Msg; FThreadCom.Msg := ''; finally FThreadCom.Unlock; end; if sMsg = 'disconnect' then Disconnect else if sMsg = 'connect' then Login else if sMsg = 'terminate' then Terminate; end; procedure TAstAMIClient.Connect; var iLast: integer; sLast: string; begin FSock.SetTimeout(5000); FSock.GetSins; FSock.Connect(FHost, FPort); iLast := FSock.LastError; sLast := FSock.LastErrorDesc; FConnected := true; end; procedure TAstAMIClient.Disconnect; begin FConnected := false; if FSock.CanRead(20) then FSock.CloseSocket; end; function TAstAMIClient.Login( AHost,APort,AUserName,APassword: string ) : boolean; begin FHost := AHost; FPort := APort; FAMIPassword := APassword; FAMIUserName := AUserName; result := Login; end; function TAstAMIClient.Login: boolean; var sSend, sResv: string; begin if not FConnected then Connect; sSend := 'Action: login' + #13#10 + 'Username: ' + 'lee' {FAMIUsername} + #13#10 + 'Secret: ' + 'test' {FAMIPassword} + #13#10 + 'Events: on' + #13#10 + #13#10; if FSock.CanWrite(50) then FSock.SendString(sSend); if FSock.CanRead(50) then sResv := FSock.RecvTerminated(2000, #13#10 + #13#10); if POS('error', lowercase(sResv)) > 0 then result := false else result := true; end; procedure TAstAMIClient.Execute; var s: string; begin repeat; // check for message to connect or disconnect to/from AMI server. if not Terminated then CheckForMsg; // if connected then do yo thing! if (FConnected) and (not Terminated) then begin s := FSock.RecvTerminated(15, #13#10#13#10); // check for error and terminate if found if ((s <> '') and (not Terminated)) then AddToOutgoing(s); // now check for incoming messages from clients if not Terminated then ProcessIncomingRequests; end else begin end; until (Terminated); end; constructor TAstAMIClient.Create( AClientList: TClientConnectionList; const AHost, APort: string; AThreadCom, AHelperThreadCom: TThreadCommunicator; AQueue: TAstMessageQueue) ; begin FreeOnTerminate := true; FClientList := AClientList; FQueue := AQueue; FThreadCom := AThreadCom; FHost := AHost; FPort := APort; FSock := TTCPBlockSocket.create; inherited Create(false); end; destructor TAstAMIClient.Destroy; begin FSock.free; //if Assigned(FQueue) then //FQueue.Lock; //try //FQueue.Free; //finally //end; inherited Destroy; end; { TOutgoingMinder } procedure TOutgoingMinder.DistributeEvent; var sl: TStringList; sPacket, sPacketData: string; iCounter: integer; begin if Terminated then exit; try sl := TStringList.create; FQueue.Lock; try if FQueue.Outgoing.Count > 0 then begin sl.Assign(FQueue.Outgoing); FQueue.Outgoing.Clear; end; finally; FQueue.Unlock; end; { At this point, we have a list of packets going out and the lock is taken off the queue so that main thread can add to it again. Now we just distribute them according to packet type (Event or Response). } for iCounter := 0 to sl.Count -1 do begin sPacketData := sl[iCounter]; // what kind of packet is it? sPacket := TAstPacketUtil.GetPacketType(sPacketData); if sPacket <> 'Response' then Self.FeedTheClientsEvents(sPacketData) else self.GiveClientResponse(sPacketData); end; finally; sl.free; end; end; procedure TOutgoingMinder.FeedTheClientsEvents(const APacket: string) ; var i, iEventLoop: integer; Conn: TClientConnection; sEventName, sPerms: string; begin if Terminated then exit; sEventName := TAstPacketUtil.GetEventName(APacket); sPerms := TAstPacketUtil.GetPacketValue(APacket, 'Privilege'); FClientList.Lock; try for i := 0 to FClientList.Count -1 do begin Conn := FClientList.Items[i]; // add packet to outgoing if accepted by filter if Conn.AcceptEvent(APacket, sEventName, sPerms) then Conn.AddOutgoing(APacket); end; finally; FClientList.UnLock; end; end; procedure TOutgoingMinder.GiveClientResponse( var APacket: string) ; var Conn: TClientConnection; sFullID, sID, sNewID: string; iPOS: integer; begin FClientList.Lock; try // replace the ActionID that contains our internal id with only ID sent // from client. sFullID := TAstPacketUtil.GetActionID(APacket); iPOS := POS('|', sFullID); sNewID := Copy(sFullID, iPOS + 1, 256); sID := Copy(sFullID, 1, iPOS - 1); APacket := StringReplace(APacket, sFullID, sNewID, [rfReplaceAll, rfIgnoreCase]); // Get reference to the clientconnection where ClientID = sID Conn := TClientConnection(FClientList.FindByProps(['ClientID'], [sID], false)); if Conn = nil then raise exception.Create('TOutgoingEventMinder.GiveClientResponse: ' + 'ClientConnection not found with supplied ID.'); // add response packet Conn.AddOutgoing(APacket); finally; FClientList.UnLock; end; end; procedure TOutgoingMinder.AddRequestsToQueue; var iCounter: integer; Client: TClientConnection; sRequest: string; begin FClientList.Lock; try for iCounter := 0 to FClientList.Count -1 do begin Client := FClientList.Items[iCounter]; // lock queue FQueue.Lock; try repeat sRequest := Client.GetIncoming; if sRequest <> '' then begin FQueue.Incoming.Add(sRequest); end; until sRequest = ''; finally; FQueue.Unlock; end; end; finally FClientList.UnLock; end; end; procedure TOutgoingMinder.Execute; begin repeat if not Terminated then begin // data coming from Asterisk AMI out through to the Proxy Clients if Terminated then exit; DistributeEvent; sleep(50); // Requests coming in from Clients to be sent to the Asterisk AMI if Terminated then exit; AddRequestsToQueue; end; sleep(50); until (Terminated); end; constructor TOutgoingMinder.Create( AClientList: TClientConnectionList; AQueue: TAstMessageQueue; AThreadCom: TThreadCommunicator) ; begin FreeOnTerminate := true; FClientList := AClientList; FQueue := AQueue; FThreadCom := AThreadCom; inherited Create(false); end; destructor TOutgoingMinder.Destroy; begin inherited Destroy; end; end.