unit webideintf; {$mode objfpc}{$H+} interface uses fpMimeTypes, Classes, SysUtils, StrUtils, httpdefs, fphttpclient,custhttpapp, fpjson, jsonparser, httproute; Const SFilesURL = '/Project/'; SIDEURL = '/IDE/'; Type TClientObject = Class(TObject) Private FID: Int64; public Procedure FromJSON(aJSON : TJSONObject); virtual; abstract; Procedure ToJSON(aJSON : TJSONObject); virtual; abstract; Property ID : Int64 Read FID Write FID; end; { TIDEClient } TIDEClient = Class(TClientObject) private FURL: String; Public Procedure FromJSON(aJSON : TJSONObject); override; Procedure ToJSON(aJSON : TJSONObject); override; Property URL : String Read FURL Write FURL; end; { TIDEExchange } TIDEExchange = Class(TClientObject) private FClientID: Int64; FName: String; FPayLoad: TJSONData; Public Destructor Destroy; override; Procedure FromJSON(aJSON : TJSONObject); override; Procedure ToJSON(aJSON : TJSONObject); override; Property ClientID : Int64 Read FClientID Write FClientID; Property Name : String Read FName Write FName; Property PayLoad : TJSONData Read FPayLoad Write FPayLoad; end; TIDEAction = Class(TIDEExchange) end; { TClientObjectList } TClientObjectList = Class(TThreadList) Public Function FindID(aID : int64) : TClientObject; end; { TIDECommand } TIDECommand = Class(TIDEExchange) private FConfirmed: Boolean; FNeedsConfirmation: Boolean; FSent: Boolean; Public Property NeedsConfirmation : Boolean Read FNeedsConfirmation Write FNeedsConfirmation; Property Sent : Boolean Read FSent Write FSent; Property Confirmed : Boolean Read FConfirmed Write FConfirmed; end; { TIDEThread } TIDEThread = Class(TThread) Private FHandler : TFPHTTPServerHandler; FExceptionClass : String; FExceptionMessage : String; Public Constructor Create(aHandler : TFPHTTPServerHandler); Procedure Execute; override; end; TIDENotification = Procedure(Sender : TObject; aExchange : TIDEExchange) of object; TIDEClientNotification = Procedure(Sender : TObject; aClient : TIDEClient) of object; TIDERequestNotification = Procedure(Sender : TObject; aURL : String) of object; { TIDEServer } TIDEServer = Class(TComponent) private FOnRequest: TIDERequestNotification; FQuitting : Boolean; FClients, FCommands, FActions : TClientObjectList; FIDCounter: Int64; FOnAction: TIDENotification; FOnClient: TIDEClientNotification; FOnClientRemoved: TIDEClientNotification; FOnConfirmCommand: TIDENotification; FProjectDir: String; FWebHandler : TFPHTTPServerHandler; FThread : TIDEThread; FLastAction : TIDEAction; FLastCommand : TIDECommand; FLastClient : TIDEClient; function CheckClient(aRequest: TRequest): INt64; procedure DeActivatedThread(Sender: TObject); function Do404(is404: boolean; aResponse: TResponse): Boolean; procedure DoEvent(aProc: TThreadMethod); procedure DoQuit(ARequest: TRequest; AResponse: TResponse); procedure DoRouteRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse); function GetAction(Index : Integer): TIDEAction; function GetActionCount: Integer; function GetPort: Integer; function GetActive: Boolean; procedure SetActive(AValue: Boolean); procedure SetPort(AValue: Integer); procedure SetProjectDir(AValue: String); Protected procedure RegisterRoutes; virtual; // HTTP request extraction procedure GetClientObjectFromRequest(ARequest: TRequest; AObject: TClientObject); function GetActionFromRequest(ARequest: TRequest): TIDEAction; function GetCommandFromRequest(ARequest: TRequest): TIDECommand; function GetClientFromRequest(ARequest: TRequest): TIDEClient; function GetJSONFromRequest(ARequest: TRequest): TJSONObject; // Sending responses procedure SendClientObjectResponse(AObject: TClientObject; AResponse: TResponse); Procedure SendJSONResponse(aJSON : TJSONObject; aResponse : TResponse); // HTTP route handlers procedure DoDeleteAction(ARequest: TRequest; AResponse: TResponse); virtual; procedure DoDeleteClient(ARequest: TRequest; AResponse: TResponse); virtual; procedure DoGetCommand(ARequest: TRequest; AResponse: TResponse);virtual; procedure DoGetFile(ARequest: TRequest; AResponse: TResponse);virtual; procedure DoPostAction(ARequest: TRequest; AResponse: TResponse);virtual; procedure DoPostClient(ARequest: TRequest; AResponse: TResponse);virtual; procedure DoPutCommand(ARequest: TRequest; AResponse: TResponse);virtual; // Event handler synchronisation. Rework this to objects Procedure DoOnAction; Procedure DoOnConfirmCommand; Procedure DoOnClientAdded; Procedure DoOnClientRemoved; Public Constructor Create(aOwner : TComponent); override; Destructor Destroy; override; Function GetNextCounter : Int64; // Public API to communicate with browser Function SendCommand(aCommand : TIDECommand) : Int64; Procedure GetClientActions(aClientID : Int64; aList : TFPList); Function DeleteAction(aID: Int64; Const aClientID : Int64 = -1): Boolean; // Public properties Property ProjectDir : String Read FProjectDir Write SetProjectDir; Property Port : Integer Read GetPort Write SetPort; Property Active : Boolean read GetActive write SetActive; Property ActionCount : Integer Read GetActionCount; Property Action[Index : Integer] : TIDEAction Read GetAction; // Events Property OnRequest : TIDERequestNotification Read FOnRequest Write FOnRequest; Property OnConfirmCommand : TIDENotification Read FOnConfirmCommand Write FOnConfirmCommand; Property OnAction : TIDENotification Read FOnAction Write FOnAction; Property OnClientAdded : TIDEClientNotification Read FOnClient Write FOnClient; Property OnClientRemoved : TIDEClientNotification Read FOnClientRemoved Write FOnClientRemoved; end; implementation { TClientObjectList } function TClientObjectList.FindID(aID: int64): TClientObject; Var L : TList; I : integer; begin Result:=Nil; L:=LockList; try I:=L.Count-1; While (Result=Nil) and (I>=0) do begin Result:=TClientObject(L[i]); if Result.ID<>aID then Result:=nil; Dec(I); end; finally UnlockList; end; end; { TIDEClient } procedure TIDEClient.FromJSON(aJSON: TJSONObject); begin FID:=aJSON.Get('id',Int64(-1)); FURL:=aJSON.Get('url',''); end; procedure TIDEClient.ToJSON(aJSON: TJSONObject); begin aJSON.Add('id',ID); aJSON.Add('url',url); end; { TIDEExchange } destructor TIDEExchange.Destroy; begin FreeAndNil(FPayload); Inherited; end; procedure TIDEExchange.FromJSON(aJSON: TJSONObject); Var P : TJSONObject; begin ID:=aJSON.Get('id',Int64(0)); Name:=aJSON.Get('name',''); P:=aJSON.Get('payload',TJSONObject(Nil)); if Assigned(P) then Payload:=aJSON.Extract('payload'); end; procedure TIDEExchange.ToJSON(aJSON: TJSONObject); begin aJSON.Add('id',ID); aJSON.Add('name',name); if Assigned(Payload) then aJSON.Add('payload',Payload.Clone); end; { TIDEThread } constructor TIDEThread.Create(aHandler: TFPHTTPServerHandler); begin FHandler:=AHandler; FreeOnTerminate:=True; Inherited Create(False); end; procedure TIDEThread.Execute; begin try FHandler.Run; FHandler:=nil; except On E : Exception do begin FExceptionClass:=E.ClassName; FExceptionMessage:=E.Message; end; end; end; { TIDEServer } function TIDEServer.GetAction(Index : Integer): TIDEAction; Var L : TList; begin L:=FActions.LockList; try Result:=TIDEAction(L.Items[Index]); finally FActions.UnlockList; end; end; procedure TIDEServer.DeActivatedThread(Sender: TObject); begin FThread:=Nil; end; function TIDEServer.GetActionCount: Integer; Var L : TList; begin L:=FActions.LockList; try Result:=L.Count; finally FActions.UnlockList; end; end; function TIDEServer.GetActive: Boolean; begin Result:=Assigned(FThread); end; function TIDEServer.GetPort: Integer; begin Result:=FWebHandler.Port; end; procedure TIDEServer.SetActive(AValue: Boolean); begin if Active=AValue then Exit; if AValue then begin FThread:=TIDEThread.Create(FWebHandler); FThread.OnTerminate:=@DeActivatedThread; end else begin FWebHandler.Terminate; // will cause thread to stop. try // Send a Quit request just in case. Normally this should fail. FQuitting:=True; TFPHTTPClient.SimpleGet(Format('http://localhost:%d/Quit',[Port])); except FQuitting:=False; end; end; end; procedure TIDEServer.SetPort(AValue: Integer); begin FWebHandler.Port:=aValue; end; procedure TIDEServer.SetProjectDir(AValue: String); begin if FProjectDir=AValue then Exit; FProjectDir:=IncludeTrailingPathDelimiter(AValue); end; procedure TIDEServer.DoOnAction; begin If Assigned(FOnAction) then FonAction(Self,FLastAction); FLastAction:=Nil; end; procedure TIDEServer.DoOnConfirmCommand; begin If Assigned(FOnAction) then FonAction(Self,FLastCommand); FLastCommand:=Nil; end; procedure TIDEServer.DoOnClientAdded; begin if Assigned(FOnClient) then FOnClient(Self,FLastClient); FLastClient:=Nil; end; procedure TIDEServer.DoOnClientRemoved; begin if Assigned(FOnClientRemoved) then FOnClientRemoved(Self,FLastClient); FLastClient:=Nil; end; procedure TIDEServer.DoGetCommand(ARequest: TRequest; AResponse: TResponse); Var L : TList; I : integer; J,C : TJSONObject; A :TJSONArray; Cmd : TIDECommand; L2 : TFPList; aClient : Int64; begin aClient:=CheckClient(aRequest); J:=nil; A:=nil; L:=FCommands.LockList; try L2:=TFPList.Create; J:=TJSONObject.Create; A:=TJSONArray.Create; J.Add('commands',A); For I:=0 to L.Count-1 do begin CMD:=TIDECommand(L[i]); if Not Cmd.Sent and (Cmd.ClientID=aClient) then begin C:=TJSONObject.Create; Cmd.ToJSON(C); A.Add(C); L2.Add(CMD); end; end; SendJSONResponse(J,aResponse); // Remove sent from list for I:=0 to L2.Count-1 do begin Cmd:=TIDECommand(L2[i]); if Cmd.NeedsConfirmation then Cmd.Sent:=True else begin Cmd.Free; L.Remove(Cmd); end; end; finally J.Free; FCommands.UnLockList; l2.Free; end; end; procedure TIDEServer.DoPutCommand(ARequest: TRequest; AResponse: TResponse); Var cmd,oCmd : TIDECommand; aID,aClient : Int64; begin aClient:=CheckClient(aRequest); aID:=StrToIntDef(aRequest.RouteParams['ID'],-1); cmd:=TIDECommand.Create; try GetClientObjectFromRequest(aRequest,Cmd); cmd.ClientID:=aClient; oCmd:=TIDECommand(FCommands.FindID(aID)); if Do404((oCmd=Nil) or (oCmd.ClientID<>aClient),aResponse) then exit; // Later on we can add more modifications oCmd.Confirmed:=True; aResponse.Code:=204; aResponse.CodeText:='OK'; aResponse.SendResponse; FLastCommand:=oCmd; DoEvent(@DoOnConfirmCommand); FCommands.Remove(oCmd); Finally cmd.Free; end; end; procedure TIDEServer.DoQuit(ARequest: TRequest; AResponse: TResponse); begin if FQuitting then aResponse.Code:=200 else aResponse.Code:=401; aResponse.SendResponse; end; procedure TIDEServer.DoRouteRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse); begin If Assigned(FonRequest) then FOnRequest(Self,aRequest.URI); end; function TIDEServer.GetJSONFromRequest(ARequest: TRequest): TJSONObject; var D : TJSONData; begin if ARequest.ContentType<>'application/json' then Raise Exception.Create('Not valid JSON payload: content type must be application/json'); D:=GetJSON(ARequest.Content); if Not (D is TJSONObject) then begin FreeAndNil(D); Raise EJSON.Create('Payload is valid JSON but not a JSON object'); end; Result:=D as TJSONObject; end; procedure TIDEServer.SendJSONResponse(aJSON: TJSONObject; aResponse: TResponse); Var JS : TJSONStringType; begin JS:=aJSON.AsJSON; aResponse.FreeContentStream:=True; aResponse.ContentStream:=TMemoryStream.Create; aResponse.ContentStream.WriteBuffer(JS[1],Length(JS)); aResponse.ContentLength:=Length(JS); aResponse.ContentType:='application/json'; aResponse.SendResponse; end; procedure TIDEServer.GetClientObjectFromRequest(ARequest: TRequest; AObject: TClientObject); Var J : TJSONObject; begin J:=GetJSONFromRequest(aRequest); try AObject.FromJSON(J); finally J.Free; end; end; procedure TIDEServer.SendClientObjectResponse(AObject: TClientObject; AResponse: TResponse); Var J : TJSONObject; begin J:=TJSONObject.Create; try aObject.ToJSON(J); SendJSONResponse(J,aResponse); finally J.Free; end; end; function TIDEServer.GetActionFromRequest(ARequest: TRequest): TIDEAction; begin Result:=TIDEAction.Create; try GetClientObjectFromRequest(aRequest,Result); except Result.Free; raise; end; end; function TIDEServer.GetCommandFromRequest(ARequest: TRequest): TIDECommand; begin Result:=TIDECommand.Create; try GetClientObjectFromRequest(aRequest,Result); except Result.Free; Raise; end; end; function TIDEServer.GetClientFromRequest(ARequest: TRequest): TIDEClient; begin Result:=TIDEClient.Create; try GetClientObjectFromRequest(aRequest,Result); except Result.Free; Raise; end; end; procedure TIDEServer.DoPostAction(ARequest: TRequest; AResponse: TResponse); var A : TIDEAction; aId,aClient : Int64; begin aClient:=CheckClient(aRequest); aID:=StrToInt64Def(aRequest.RouteParams['ID'],-1); Try A:=GetACtionFromRequest(aRequest); A.ClientID:=aClient; if A.ID=0 then a.ID:=aID; FActions.Add(A); FLastAction:=A; DoEvent(@DoOnAction); AResponse.Code:=201; AResponse.Codetext:='Created'; except On E: Exception do begin AResponse.Code:=400; AResponse.Codetext:='Invalid Param'; AResponse.Content:='Invalid data ('+E.ClassName+'): '+E.Message; end; end; aResponse.SendResponse; end; function TIDEServer.CheckClient(aRequest: TRequest): INt64; Var S : String; begin S:=ARequest.RouteParams['Client']; if (S='') then Raise EJSON.Create('Missing client ID in request'); if Not TryStrToInt64(S,Result) then Raise EJSON.CreateFmt('Invalid client ID: %s',[S]); end; procedure TIDEServer.DoDeleteAction(ARequest: TRequest; AResponse: TResponse); var SID : String; ID,aClient : Int64; begin Try aClient:=CheckClient(ARequest); SID:=ARequest.RouteParams['ID']; ID:=StrtoInt64Def(SID,-1); if Do404((ID=-1) or not (DeleteAction(ID,aClient)),aResponse) then exit; AResponse.Code:=204; AResponse.Codetext:='No content'; aResponse.SendResponse; except On E: Exception do begin AResponse.Code:=400; AResponse.Codetext:='Invalid Param'; AResponse.Content:='Invalid data ('+E.ClassName+'): '+E.Message; end; end; end; procedure TIDEServer.DoGetFile(ARequest: TRequest; AResponse: TResponse); Var FN : String; begin FN:=ARequest.URL; if AnsiStartsText(SFilesURL,FN) then Delete(FN,1,Length(SFilesURL)); FN:=ExpandFileName(FProjectDir+FN); if Pos('..',ExtractRelativepath(FProjectDir,FN))<>0 then begin aResponse.Code:=401; aResponse.CodeText:='Forbidden'; aResponse.Content:='