{*******************************************************}
{                                                       }
{       Tocsg.WTS                                       }
{                                                       }
{       Copyright (C) 2022 sunk                         }
{                                                       }
{*******************************************************}

unit Tocsg.WTS;

interface

uses
  WinApi.Windows, SysUtils, Generics.Collections, EM.WtsApi32;

const
  NEAR_SUCCESS = 0;

type
  PWTSSessionEntryInfo = ^TWTSSessionEntryInfo;
  TWTSSessionEntryInfo = record
    dwSessionId: DWORD;
    sWinStationName: String;
  end;

  TTgWTSSessionInfomation = class(TObject)
  protected
    lstSessionID_: TList<PWTSSessionEntryInfo>;

    procedure OnSessionInfo(Sender: TObject; const Item: PWTSSessionEntryInfo;
                    Action: TCollectionNotification);
    function GetCount: Integer;
    function GetUserNameByIndex(nIndex: Integer): String;
    function GetSessionIdByIndex(nIndex: Integer): DWORD;
  public
    Constructor Create;
    Destructor Destroy; override;

    procedure UpdateSessionInfo;

    function GetUserNameBySsid(const dwSsid: DWORD): String;

    property Count: Integer read GetCount;
    property SessionIDs[nIndex: Integer]: DWORD read GetSessionIdByIndex;
    property UserNames[nIndex: Integer]: String read GetUserNameByIndex;
  end;

const
  UF_SCRIPT                          = $0001;
  {$EXTERNALSYM UF_SCRIPT}
  UF_ACCOUNTDISABLE                  = $0002;
  {$EXTERNALSYM UF_ACCOUNTDISABLE}
  UF_HOMEDIR_REQUIRED                = $0008;
  {$EXTERNALSYM UF_HOMEDIR_REQUIRED}
  UF_LOCKOUT                         = $0010;
  {$EXTERNALSYM UF_LOCKOUT}
  UF_PASSWD_NOTREQD                  = $0020;
  {$EXTERNALSYM UF_PASSWD_NOTREQD}
  UF_PASSWD_CANT_CHANGE              = $0040;
  {$EXTERNALSYM UF_PASSWD_CANT_CHANGE}
  UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = $0080;
  {$EXTERNALSYM UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED}

//
// Account type bits as part of usri_flags.
//

  UF_TEMP_DUPLICATE_ACCOUNT    = $0100;
  {$EXTERNALSYM UF_TEMP_DUPLICATE_ACCOUNT}
  UF_NORMAL_ACCOUNT            = $0200;
  {$EXTERNALSYM UF_NORMAL_ACCOUNT}
  UF_INTERDOMAIN_TRUST_ACCOUNT = $0800;
  {$EXTERNALSYM UF_INTERDOMAIN_TRUST_ACCOUNT}
  UF_WORKSTATION_TRUST_ACCOUNT = $1000;
  {$EXTERNALSYM UF_WORKSTATION_TRUST_ACCOUNT}
  UF_SERVER_TRUST_ACCOUNT      = $2000;
  {$EXTERNALSYM UF_SERVER_TRUST_ACCOUNT}

  UF_MACHINE_ACCOUNT_MASK = UF_INTERDOMAIN_TRUST_ACCOUNT or UF_WORKSTATION_TRUST_ACCOUNT or UF_SERVER_TRUST_ACCOUNT;
  {$EXTERNALSYM UF_MACHINE_ACCOUNT_MASK}

  UF_ACCOUNT_TYPE_MASK = UF_TEMP_DUPLICATE_ACCOUNT or UF_NORMAL_ACCOUNT or UF_INTERDOMAIN_TRUST_ACCOUNT or UF_WORKSTATION_TRUST_ACCOUNT or UF_SERVER_TRUST_ACCOUNT;
  {$EXTERNALSYM UF_ACCOUNT_TYPE_MASK}

  UF_DONT_EXPIRE_PASSWD                     = $10000;
  {$EXTERNALSYM UF_DONT_EXPIRE_PASSWD}
  UF_MNS_LOGON_ACCOUNT                      = $20000;
  {$EXTERNALSYM UF_MNS_LOGON_ACCOUNT}
  UF_SMARTCARD_REQUIRED                     = $40000;
  {$EXTERNALSYM UF_SMARTCARD_REQUIRED}
  UF_TRUSTED_FOR_DELEGATION                 = $80000;
  {$EXTERNALSYM UF_TRUSTED_FOR_DELEGATION}
  UF_NOT_DELEGATED                          = $100000;
  {$EXTERNALSYM UF_NOT_DELEGATED}
  UF_USE_DES_KEY_ONLY                       = $200000;
  {$EXTERNALSYM UF_USE_DES_KEY_ONLY}
  UF_DONT_REQUIRE_PREAUTH                   = $400000;
  {$EXTERNALSYM UF_DONT_REQUIRE_PREAUTH}
  UF_PASSWORD_EXPIRED                       = DWORD($800000);
  {$EXTERNALSYM UF_PASSWORD_EXPIRED}
  UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = $1000000;
  {$EXTERNALSYM UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION}

  UF_SETTABLE_BITS =
                    UF_SCRIPT or
                    UF_ACCOUNTDISABLE or
                    UF_LOCKOUT or
                    UF_HOMEDIR_REQUIRED or
                    UF_PASSWD_NOTREQD or
                    UF_PASSWD_CANT_CHANGE or
                    UF_ACCOUNT_TYPE_MASK or
                    UF_DONT_EXPIRE_PASSWD or
                    UF_MNS_LOGON_ACCOUNT or
                    UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED or
                    UF_SMARTCARD_REQUIRED or
                    UF_TRUSTED_FOR_DELEGATION or
                    UF_NOT_DELEGATED or
                    UF_USE_DES_KEY_ONLY or
                    UF_DONT_REQUIRE_PREAUTH or
                    UF_PASSWORD_EXPIRED or
                    UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION;
  {$EXTERNALSYM UF_SETTABLE_BITS}

  ENCRYPTED_PWLEN = 16;

type
  _USER_INFO_1 = record
    usri1_name: LPWSTR;
    usri1_password: LPWSTR;
    usri1_password_age: DWORD;
    usri1_priv: DWORD;
    usri1_home_dir: LPWSTR;
    usri1_comment: LPWSTR;
    usri1_flags: DWORD;
    usri1_script_path: LPWSTR;
  end;
  {$EXTERNALSYM _USER_INFO_1}
  USER_INFO_1 = _USER_INFO_1;
  {$EXTERNALSYM USER_INFO_1}
  TUserInfo1 = USER_INFO_1;
  PUserInfo1 = ^TUserInfo1;

  _USER_INFO_2 = record
    usri2_name: LPWSTR;
    usri2_password: LPWSTR;
    usri2_password_age: DWORD;
    usri2_priv: DWORD;
    usri2_home_dir: LPWSTR;
    usri2_comment: LPWSTR;
    usri2_flags: DWORD;
    usri2_script_path: LPWSTR;
    usri2_auth_flags: DWORD;
    usri2_full_name: LPWSTR;
    usri2_usr_comment: LPWSTR;
    usri2_parms: LPWSTR;
    usri2_workstations: LPWSTR;
    usri2_last_logon: DWORD;
    usri2_last_logoff: DWORD;
    usri2_acct_expires: DWORD;
    usri2_max_storage: DWORD;
    usri2_units_per_week: DWORD;
    usri2_logon_hours: PBYTE;
    usri2_bad_pw_count: DWORD;
    usri2_num_logons: DWORD;
    usri2_logon_server: LPWSTR;
    usri2_country_code: DWORD;
    usri2_code_page: DWORD;
  end;
  {$EXTERNALSYM _USER_INFO_2}
  USER_INFO_2 = _USER_INFO_2;
  {$EXTERNALSYM USER_INFO_2}
  TUserInfo2 = USER_INFO_2;
  PUserInfo2 = ^TUserInfo2;

  _USER_INFO_22 = record
    usri22_name: LPWSTR;
    usri22_password: array [0..ENCRYPTED_PWLEN - 1] of BYTE;
    usri22_password_age: DWORD;
    usri22_priv: DWORD;
    usri22_home_dir: LPWSTR;
    usri22_comment: LPWSTR;
    usri22_flags: DWORD;
    usri22_script_path: LPWSTR;
    usri22_auth_flags: DWORD;
    usri22_full_name: LPWSTR;
    usri22_usr_comment: LPWSTR;
    usri22_parms: LPWSTR;
    usri22_workstations: LPWSTR;
    usri22_last_logon: DWORD;
    usri22_last_logoff: DWORD;
    usri22_acct_expires: DWORD;
    usri22_max_storage: DWORD;
    usri22_units_per_week: DWORD;
    usri22_logon_hours: PBYTE;
    usri22_bad_pw_count: DWORD;
    usri22_num_logons: DWORD;
    usri22_logon_server: LPWSTR;
    usri22_country_code: DWORD;
    usri22_code_page: DWORD;
  end;
  {$EXTERNALSYM _USER_INFO_22}
  USER_INFO_22 = _USER_INFO_22;
  {$EXTERNALSYM USER_INFO_22}
  TUserInfo22 = USER_INFO_22;
  PUserInfo22 = ^TUserInfo22;

// 사용자 계정에 띄어쓰기가 있는경우...
// WTSQuerySessionInformation()만으로는 풀네임을 구해올수 없다. 그래서 추가
  PUserInfo23 = ^TUserInfo23;
  _USER_INFO_23 = record
    usri23_name: LPWSTR;
    usri23_full_name: LPWSTR;
    usri23_comment: LPWSTR;
    usri23_flags: DWORD;
    usri23_user_sid: PSID;
  end;
  {$EXTERNALSYM _USER_INFO_23}
  TUserInfo23 = _USER_INFO_23;
  USER_INFO_23 = _USER_INFO_23;
  {$EXTERNALSYM USER_INFO_23}

  TNetUserGetInfo = function(sServerName: PChar; sUserName: PChar; dwLevel: DWORD; var pBuf: Pointer): DWORD; stdcall;
  TNetApiBufferFree = function(pBuf: Pointer): Integer; stdcall;
  TNetUserChangePassword = function(sServerName, sUserName, sOldPass, sNewPass: PChar): DWORD; stdcall;

function NetUserGetInfo(sServerName: PChar; sUserName: PChar; dwLevel: DWORD; var pBuf: Pointer): DWORD;
function NetApiBufferFree(pBuf : Pointer) :Integer;

function NetUserChangePassword(sServerName, sUserName, sOldPass, sNewPass: String): DWORD;

function GetLastChangePasswordDT(sServerName, sUserName: String): TDateTime;
function GetLastLogOnDT(sServerName, sUserName: String): TDateTime;
function GetLastLogOnDTandBadCnt(sServerName, sUserName: String; var dtLastLogOn: TDateTime; var nBadCnt: Integer): Boolean;
function HasAccountPassword(sServerName, sUserName: String): Boolean;

function WTSGetActiveConsoleSessionId: DWORD; stdcall external 'Kernel32.dll';
function WTS_GetString(dwSessionID: DWORD; const aWTSInfoClass: TWtsInfoClass): String;
function WTS_GetDWORD(dwSessionID: DWORD; const aWTSInfoClass: TWtsInfoClass): DWORD;
function WTS_GetUserNameFromSessionID(dwSessionID: DWORD): String;
function WTS_GetCurrentUserName: String;
function WTS_GetActiveSessionUserName: String;

implementation

uses
  Tocsg.Exception, System.DateUtils, Tocsg.DateTime;

var
  _hNetApi32: THandle = 0;
  _fnNetUserGetInfo: TNetUserGetInfo = nil;
  _fnNetApiBufferFree: TNetApiBufferFree = nil;
  _fnNetUserChangePassword: TNetUserChangePassword = nil;

function InitNetApi32Procedure: Boolean;
begin
  if _hNetApi32 = 0 then
  begin
//    _hNetApi32 := GetModuleHandle('netapi32.dll');
    _hNetApi32 := LoadLibrary('netapi32.dll');
    if _hNetApi32 <> 0 then
    begin
      @_fnNetUserGetInfo := GetProcAddress(_hNetApi32, 'NetUserGetInfo');
      @_fnNetApiBufferFree := GetProcAddress(_hNetApi32, 'NetApiBufferFree');
      @_fnNetUserChangePassword := GetProcAddress(_hNetApi32, 'NetUserChangePassword');
    end;
  end;
  Result := _hNetApi32 <> 0;
end;

function NetUserGetInfo(sServerName, sUserName: PChar; dwLevel: DWORD; var pBuf: Pointer): DWORD;
begin
  Result := DWORD(-1);
  if InitNetApi32Procedure and Assigned(_fnNetUserGetInfo) then
    Result := _fnNetUserGetInfo(sServerName, sUserName, dwLevel, pBuf);
end;

function NetApiBufferFree(pBuf: Pointer) :Integer;
begin
  Result := -1;
  if InitNetApi32Procedure and Assigned(_fnNetUserGetInfo) then
    Result := _fnNetApiBufferFree(pBuf);
end;

function NetUserChangePassword(sServerName, sUserName, sOldPass, sNewPass: String): DWORD;
begin
  Result := DWORD(-1);
  if InitNetApi32Procedure and Assigned(_fnNetUserChangePassword) then
    Result := _fnNetUserChangePassword(PChar(sServerName), PChar(sUserName), PChar(sOldPass), PChar(sNewPass));
end;

function GetLastChangePasswordDT(sServerName, sUserName: String): TDateTime;
var
  pInfo: PUserInfo1;
begin
  Result := 0;
  try
    pInfo := nil;
    if NetUserGetInfo(PChar(sServerName), PChar(sUserName), 1, Pointer(pInfo)) = 0 then
    begin
      Result := IncSecond(Now, pInfo.usri1_password_age * -1);
      NetApiBufferFree(pInfo);
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetLastChangePasswordDT()');
  end;
end;

function GetLastLogOnDT(sServerName, sUserName: String): TDateTime;
var
  pInfo: PUserInfo2;
begin
  Result := 0;
  try
    pInfo := nil;
    if NetUserGetInfo(PChar(sServerName), PChar(sUserName), 2, Pointer(pInfo)) = 0 then
    begin
      Result := ConvTimeToDateTime(pInfo.usri2_last_logon);
      NetApiBufferFree(pInfo);
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetLastLogOnDT()');
  end;
end;

function GetLastLogOnDTandBadCnt(sServerName, sUserName: String; var dtLastLogOn: TDateTime; var nBadCnt: Integer): Boolean;
var
  pInfo: PUserInfo2;
begin
  Result := false;
  dtLastLogOn := 0;
  nBadCnt := 0;
  try
    pInfo := nil;
    if NetUserGetInfo(PChar(sServerName), PChar(sUserName), 2, Pointer(pInfo)) = 0 then
    begin
      dtLastLogOn := ConvTimeToDateTime(pInfo.usri2_last_logon);
      nBadCnt := pInfo.usri2_bad_pw_count;
      NetApiBufferFree(pInfo);
      Result := true;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetLastLogOnDTandBadCnt()');
  end;
end;

// 임계치 초과 시 계정이 잠긴다 24_0109 13:00:48 kku
function HasAccountPassword(sServerName, sUserName: String): Boolean;
var
  bRet: Boolean;
  hToken: THandle;
begin
  try
    bRet := LogonUser(PChar(sUserName), PChar(sServerName), '',
                                LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, hToken);
    Result := not bRet and (GetLastError <> 1327);

  // 음 이걸로 체크하면 false 시 최근 비번 설정일이 변경된다...
//    Result := NetUserChangePassword(sServerName, sUserName, '', '') = 86;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. HasAccountPassword()');
  end;
end;

{ TTgWTSSessionInfomation }

Constructor TTgWTSSessionInfomation.Create;
begin
  Inherited Create;
  lstSessionID_ := TList<PWTSSessionEntryInfo>.Create;
  lstSessionID_.OnNotify := OnSessionInfo;
  UpdateSessionInfo;
end;

Destructor TTgWTSSessionInfomation.Destroy;
begin
  FreeAndNil(lstSessionID_);
  Inherited;
end;

procedure TTgWTSSessionInfomation.OnSessionInfo(Sender: TObject; const Item: PWTSSessionEntryInfo;
                Action: TCollectionNotification);
begin
  case Action of
    cnAdded: ;
    cnRemoved: Dispose(Item);
    cnExtracted: ;
  end;
end;

function TTgWTSSessionInfomation.GetUserNameByIndex(nIndex: Integer): String;
begin
  Result := '';
  if (nIndex >= 0) and (nIndex < lstSessionID_.Count) then
    Result := WTS_GetUserNameFromSessionID(lstSessionID_[nIndex].dwSessionId);
end;

function TTgWTSSessionInfomation.GetSessionIdByIndex(nIndex: Integer): DWORD;
begin
  Result := 0;
  if (nIndex >= 0) and (nIndex < lstSessionID_.Count) then
    Result := lstSessionID_[nIndex].dwSessionId;
end;

function TTgWTSSessionInfomation.GetCount: Integer;
begin
  Result := lstSessionID_.Count;
end;

procedure TTgWTSSessionInfomation.UpdateSessionInfo;
var
  pwsi, iter: PWtsSessionInfo;
  i, dwCount: DWORD;
  pInfo: PWTSSessionEntryInfo;
begin
  lstSessionID_.Clear;
  if WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, pwsi, dwCount) then
  begin
    iter := pwsi;
    for i := 0 to dwCount - 1 do
    begin
      New(pInfo);
      pInfo.dwSessionId := iter.SessionId;
      pInfo.sWinStationName := iter.pWinStationName;
      lstSessionID_.Add(pInfo);
      Inc(iter);
    end;
    WTSFreeMemory(pwsi);
  end;
end;

function TTgWTSSessionInfomation.GetUserNameBySsid(const dwSsid: DWORD): String;
var
  i: Integer;
begin
  Result := '';
  for i := 0 to lstSessionID_.Count - 1 do
    if lstSessionID_[i].dwSessionId = dwSsid then
    begin
      Result := lstSessionID_[i].sWinStationName;
      exit;
    end;
end;

{ other }

function WTS_GetString(dwSessionID: DWORD; const aWTSInfoClass: TWtsInfoClass): String;
var
  pBuf     : Pointer;
  dwReturn : DWORD;
begin
  Result := '';
  pBuf := nil;
  dwReturn := 0;
// WTS_INFO_CLASS이 형식이 꼬였는지.. 그대로 넘겨주면 안된다.. 그래서 이렇게 두번 캐스팅 2012-01-09 sunk
  if WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, dwSessionID,
                                aWTSInfoClass, pBuf, dwReturn) then
  begin
    if pBuf <> nil then
    begin
      Result := PChar(pBuf);
      WTSFreeMemory(pBuf);
    end;
  end;
end;

function WTS_GetDWORD(dwSessionID: DWORD; const aWTSInfoClass: TWtsInfoClass): DWORD;
var
  pBuf     : Pointer;
  dwReturn : DWORD;
begin
  Result := 0;
  pBuf := nil;
  dwReturn := 0;
// WTS_INFO_CLASS이 형식이 꼬였는지.. 그대로 넘겨주면 안된다.. 그래서 이렇게 두번 캐스팅 2012-01-09 sunk
  if WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, dwSessionID,
                                aWTSInfoClass, pBuf, dwReturn) then
  begin
    if pBuf <> nil then
    begin
      Result := DWORD(pBuf);
      WTSFreeMemory(pBuf);
    end;
  end;
end;

function WTS_GetUserNameFromSessionID(dwSessionID: DWORD): String;
begin
  Result := WTS_GetString(dwSessionID, WTSUserName);
end;

function WTS_GetCurrentUserName: String;
var
  pBuf: Pointer;
  sResult: String;
begin
  Result := Trim(WTS_GetUserNameFromSessionID(WTS_CURRENT_SESSION));
  if NetUserGetInfo(nil, PChar(Result), 23, pBuf) = NEAR_SUCCESS then
  begin
    try
      if PUserInfo23(pBuf).usri23_full_name <> '' then
      begin
        SetLength(sResult, Length(PUserInfo23(pBuf).usri23_full_name));
        StrCopy(PChar(sResult), PUserInfo23(pBuf).usri23_full_name);
        Result := Trim(sResult);
      end;
      NetApiBufferFree(pBuf);
    except
    //
      exit;
    end;
  end;
end;

function WTS_GetActiveSessionUserName: String;
begin
  Result := WTS_GetUserNameFromSessionID(WTSGetActiveConsoleSessionId);
end;

//finalization
//  if _hNetApi32 <> 0 then
//    FreeLibrary(_hNetApi32);

end.

