I'd like to get a MAC address from an IP of a host in the same local network. I'd prefer to get this information from the local cache instead of sending a new ARP ARP request. I found that ResolveIpNetEntry2 should be what I need.
Unfortunately I didn't find any code sample for Delphi with that function. Even worse I didn't even find any Delphi headers for that function and its data types. So I tried converting them myself. Well, it compiles, but I get ERROR_INVALID_PARAMETER (87), so apparently I converted something wrong.
Could someone please tell me how to correct it?
const
IF_MAX_PHYS_ADDRESS_LENGTH = 32;
type
NET_LUID = record
case Word of
1: (Value: Int64;);
2: (Reserved: Int64;);
3: (NetLuidIndex: Int64;);
4: (IfType: Int64;);
end;
NL_NEIGHBOR_STATE = (
NlnsUnreachable=0,
NlnsIncomplete,
NlnsProbe,
NlnsDelay,
NlnsStale,
NlnsReachable,
NlnsPermanent,
NlnsMaximum);
PMIB_IPNET_ROW2 = ^MIB_IPNET_ROW2;
MIB_IPNET_ROW2 = record
Address: LPSOCKADDR; //SOCKADDR_INET
InterfaceIndex: ULONG; //NET_IFINDEX
InterfaceLuid: NET_LUID;
PhysicalAddress: array [0..IF_MAX_PHYS_ADDRESS_LENGTH - 1] of UCHAR;
PhysicalAddressLength: ULONG;
State: NL_NEIGHBOR_STATE;
Union: record
case Integer of
0: (IsRouter: Boolean;
IsUnreachable: Boolean);
1: (Flags: UCHAR);
end;
ReachabilityTime: record
case Integer of
0: (LastReachable: ULONG);
1: (LastUnreachable: ULONG);
end;
end;
function ResolveIp(const AIp: String; AIfIndex: ULONG): String;
type
TResolveIpNetEntry2Func = function (Row: PMIB_IPNET_ROW2; const SourceAddress: LPSOCKADDR): DWORD; stdcall; //NETIOAPI_API
const
IphlpApiDll = 'iphlpapi.dll';
var
hIphlpApiDll: THandle;
ResolveIpNetEntry2: TResolveIpNetEntry2Func;
dw: DWORD;
Row: PMIB_IPNET_ROW2;
SourceAddress: LPSOCKADDR;
IpAddress: LPSOCKADDR;
begin
hIphlpApiDll := LoadLibrary(IphlpApiDll);
if hIphlpApiDll = 0 then
Exit;
ResolveIpNetEntry2 := GetProcAddress(hIphlpApiDll, 'ResolveIpNetEntry2');
if (#ResolveIpNetEntry2 = nil) then
Exit;
IpAddress := AllocMem(SizeOf(IpAddress));
IpAddress.sa_family := AF_INET;
IpAddress.sa_data := PAnsiChar(AIp);
Row := AllocMem(SizeOf(Row));
Row.Address := IpAddress;
Row.InterfaceIndex := AIfIndex;
SourceAddress := 0;
dw := ResolveIpNetEntry2(Row, SourceAddress);
//...
end;
Per the ResolveIpNetEntry2() documentation:
If the function fails, the return value is one of the following error codes.
...
ERROR_INVALID_PARAMETER
An invalid parameter was passed to the function. This error is returned if a NULL pointer is passed in the Row parameter, the Address member of the MIB_IPNET_ROW2 pointed to by the Row parameter was not set to a valid IPv4 or IPv6 address, or both the InterfaceLuid or InterfaceIndex members of the MIB_IPNET_ROW2 pointed to by the Row parameter were unspecified. This error is also returned if a loopback address was passed in the Address member.
Your translation of the API structures is incorrect in general. You got several fields wrong, which affects field offsets and sizes. Such as the bitfields. But more importantly, your declaration of the MIB_IPNET_ROW2.Address field is completely wrong. It is not a pointer to an external SOCKADDR record at all. It is a SOCKADDR_INET record that exists inside of the MIB_IPNET_ROW2 itself, not externally.
You are also not allocating or initializing your memory blocks correctly, which also plays into the above documentation.
And, even if you had the API translated correctly, you are not converting your AIp string to a SOCKADDR correctly, either. You can't simply type-cast it, you need to actually convert it from string characters to a network address integer, using inet_addr(), InetPton(), or other equivalent function. So that also plays into the above documentation.
You are lucky your code did not crash altogether.
Try something more like this instead:
{$MINENUMSIZE 4}
const
IF_MAX_PHYS_ADDRESS_LENGTH = 32;
type
NET_LUID_INFO = record
Reserved: array [0..2] of UCHAR; // ULONG64:24
NetLuidIndex: array [0..2] of UCHAR; // ULONG64:24
IfType: array [0..1] of UCHAR; // ULONG64:16
//
// TODO: if you need to access these values, define
// some property getters/setters to translate them
// to/from UInt64...
end;
NET_LUID = record
case Integer of
0: (Value: UInt64);
1: (Info: NET_LUID_INFO);
end;
NL_NEIGHBOR_STATE = (
NlnsUnreachable = 0,
NlnsIncomplete,
NlnsProbe,
NlnsDelay,
NlnsStale,
NlnsReachable,
NlnsPermanent,
NlnsMaximum);
PSOCKADDR_INET = ^SOCKADDR_INET;
SOCKADDR_INET = record
case Integer of
0: (Ipv4: SOCKADDR_IN);
1: (Ipv6: SOCKADDR_IN6);
2: (si_family: ADDRESS_FAMILY);
end;
NETIO_STATUS = DWORD;
NET_IFINDEX = ULONG;
PMIB_IPNET_ROW2 = ^MIB_IPNET_ROW2;
MIB_IPNET_ROW2 = record
Address: SOCKADDR_INET;
InterfaceIndex: NET_IFINDEX;
InterfaceLuid: NET_LUID;
PhysicalAddress: array [0..IF_MAX_PHYS_ADDRESS_LENGTH - 1] of UCHAR;
PhysicalAddressLength: ULONG;
State: NL_NEIGHBOR_STATE;
Flags: UCHAR;
ReachabilityTime: record
case Integer of
0: (LastReachable: ULONG);
1: (LastUnreachable: ULONG);
end;
function IsRouter: Boolean;
function IsUnreachable;
end;
function MIB_IPNET_ROW2.IsRouter: Boolean;
begin
Result := (Flags and $01) <> 0;
end;
function MIB_IPNET_ROW2.IsUnreachable;
begin
Result := (Flags and $02) <> 0;
end;
function ResolveIp(const AIp: String; AIfIndex: ULONG): String;
type
TResolveIpNetEntry2Func = function (Row: PMIB_IPNET_ROW2; const SourceAddress: PSOCKADDR_INET): NETIO_STATUS; stdcall;
const
IphlpApiDll = 'iphlpapi.dll';
var
hIphlpApiDll: THandle;
ResolveIpNetEntry2: TResolveIpNetEntry2Func;
status: NETIO_STATUS;
Row: PMIB_IPNET_ROW2;
begin
Result := '';
hIphlpApiDll := LoadLibrary(IphlpApiDll);
if hIphlpApiDll = 0 then
Exit;
try
#ResolveIpNetEntry2 := GetProcAddress(hIphlpApiDll, 'ResolveIpNetEntry2');
if not Assigned(ResolveIpNetEntry2) then
Exit;
New(Row);
try
ZeroMemory(Row, SizeOf(MIB_IPNET_ROW2));
if InetPton(AF_INET, PChar(AIp), #(Row.Address.Ipv4.sin_addr)) = 1 then
Row.Address.Ipv4.sin_family := AF_INET
else
if InetPton(AF_INET6, PChar(AIp), #(Row.Address.Ipv6.sin6_addr)) = 1 then
Row.Address.Ipv6.sin6_family := AF_INET6
else
Exit;
Row.InterfaceIndex := AIfIndex;
status := ResolveIpNetEntry2(Row, nil);
//...
finally
Dispose(Row);
end;
finally
FreeLibrary(hIphlpApiDll);
end;
end;
Alternatively, ResolveIp() can be written without dynamically allocating the Row at all:
function ResolveIp(const AIp: String; AIfIndex: ULONG): String;
...
var
...
Row: MIB_IPNET_ROW2;
begin
...
ZeroMemory(#Row, SizeOf(Row));
...
status := ResolveIpNetEntry2(#Row, nil);
...
end;
Related
I have several hardcoded validations like these:
const
cLstAct = 1;
cLstOrg = 4;
cLstClockAct = 11;
const
FUNCT_1 = 224;
FUNCT_2 = 127;
FUNCT_3 = 3;
if lFuncID in [FUNCT_1,FUNCT_2,FUNCT_3] then ...
if not (lListType in [cLstAct..cLstOrg,cLstClockAct]) then ...
if not (lPurpose in [0..2]) then ...
that I want to replace with a common method like
function ValidateInSet(AIntValue: integer; AIntSet: ###): Boolean;
begin
Result := (AIntValue in AIntSet);
if not Result then ...
end;
but what type to choose for AIntSet?
Currently the values to be tested throughout the code go up to a const value 232 (so I can e.g. use a TByteSet = Set of Byte), but I can foresee that we will bump into the E1012 Constant expression violates subrange bounds when the constant values exceed 255.
My Google-fu fails me here...
(Currently on Delphi Seattle Update 1)
Use a dictionary, TDictionary<Integer, Integer>. The value is irrelevant and you only care about the key. If the dictionary contains a specific key then that key is a member of the set. Use AddOrSetValue to add a member, Remove to delete a member and ContainsKey to test membership.
The point of using a dictionary is that it gives you O(1) lookup.
You don't want to use this type directly as a set. You should wrap it in a class that just exposes set like capabilities. An example of that can be found here: https://stackoverflow.com/a/33530037/505088
You can use an array of Integer:
function ValidateInSet(AIntValue: integer; AIntSet: array of Integer): Boolean;
var
I: Integer;
begin
Result := False;
for I := Low(AIntSet) to High(AIntSet) do
begin
if AIntSet[I] = AIntValue then
begin
Result := True;
Break;
end;
end;
if not Result then ...
end;
const
cLstAct = 1;
cLstOrg = 4;
cLstClockAct = 11;
const
FUNCT_1 = 224;
FUNCT_2 = 127;
FUNCT_3 = 3;
if ValidateInSet(lFuncID, [FUNCT_1, FUNCT_2, FUNCT_3]) then ...
if not ValidateInSet(lListType, [cLstAct, 2, 3, cLstOrg, cLstClockAct]) then ...
if not ValidateInSet(lPurpose, [0, 1, 2]) then ...
If you are on a recent Delphi version, you can use TArray<Integer>.
function ValidateInSet(AIntValue: integer; const AIntSet: TArray<Integer>): Boolean;
var
N: Integer;
begin
{ option1 : if AIntSet is always sorted }
result := TArray.BinarySearch(AIntSet, AIntValue, N);
{ option 2: works for any array }
result := false;
for N in AIntSet do begin
if AIntValue = N then begin
result := true;
Break;
end;
end;
if not Result then begin
// ...
end;
end;
Calling is merely the same as with a set (except for ranges):
if ValidateInSet(lFuncID, [FUNCT_1,FUNCT_2,FUNCT_3]) then begin
end;
The direct answer would be TBits class
http://docwiki.embarcadero.com/Libraries/Seattle/en/System.Classes.TBits.Bits
Note: This can only be used starting with Delphi XE4 though - http://qc.embarcadero.com/wc/qcmain.aspx?d=108829
However for your "Set of integers" it in most inflated case would take 2^31 / 8 bytes of memory (because negative values of integer would not be even considered), and that would be a lot...
So I hope you would never really want to have a set of the whole integer. Or you should invest into Sparse Arrays instead.
function ValidateInSet(const AIntValue: integer; const AIntSet: TBits): Boolean;
begin
Result := (AIntValue >= 0) and (AIntValue < AIntSet.Size);
if Result then
Result := AIntSet.Bits[AIntValue];
if not Result then ...
v-a-l-i-d-a-t-e
end;
or rather
function ValidateInSet(const AIntValue: integer; const AIntSet: TBits): Boolean;
begin
Result := false;
if AIntValue < 0 then exit; // Validation criterion #1
if AIntValue >= AIntSet.Size then exit; // Validation criterion #2
if not AIntSet.Bits[AIntValue] then exit; // Validation criterion #3
if .... then exit; // Validation criterion #4
if .... then exit; // Validation criterion #5
if .... then exit; // Validation criterion #6
Result := true;
end;
or perhaps
TSetTestCriterion = TFunc<Integer, Boolean>;
TSetTestCriteria = TArray<TFunc<Integer, Boolean>>;
function ValidateInSet(const AIntValue: integer;
const AIntSet: TBits; const Tests: TSetTestCriteria = nil): Boolean;
var ExtraTest: TSetTestCriterion;
begin
Result := false;
if AIntValue < 0 then exit; // Validation criterion #1
if AIntValue >= AIntSet.Size then exit; // Validation criterion #2
if not AIntSet.Bits[AIntValue] then exit; // Validation criterion #3
if Tests <> nil then // Validation criteria #4, #5, #6, ...
for ExtraTest in Tests do
if not ExtraTest(AIntValue) then exit;
Result := true;
end;
http://docwiki.embarcadero.com/Libraries/Seattle/en/System.SysUtils.TFunc
Now - just for demo, in real app you would create those set and array once and cache for long (forever, or at least unless the configuration change would demand rebuilding them).
Type FuncIDs = ( FUNCT_3 = 3, FUNCT_2 = 127, FUNCT_1 = 224);
var MysticGlobalFlag: Boolean;
function ValidateFuncID( const lFuncID: FuncIDs): Boolean;
var map: TBits;
begin
map := TBits.Create;
try
map.Size := High(lFuncID) + 1;
map.Bits[ Ord(Func_1) ] := True;
map.Bits[ Ord(Func_2) ] := True;
map.Bits[ Ord(Func_3) ] := True;
Result := ValidateInSet( Ord(lFuncID), map,
TSetTestCriteria.Create(
function( lFuncID: integer) : Boolean
begin
Result := MysticGlobalFlag or (lFuncID <> Ord(FuncIDs.FUNC_2))
end
,
function( lFuncID: integer) : Boolean
begin
Result := (lFuncID <> Ord(FuncIDs.FUNC_3)) or (DayOfTheWeek(Now()) = 4)
end
)
);
finally
map.Destroy;
end;
if not Result then // from the original question code
... // seems like a placeholder for error handling or object creation and registration
end;
All, I know it's years since people answered this, but here is a new solution using Delphi generics: -
interface
uses
System.Generics.Defaults;
type
TUtilityArray<T> = class
public
class function Contains(const x : T; const an_array : array of T) : boolean;
end;
implementation
class function TUtilityArray<T>.Contains(const x: T; const an_array: array of T): boolean;
var
y : T;
l_comparer : IEqualityComparer<T>;
begin
Result := false;
l_comparer := TEqualityComparer<T>.Default;
for y in an_array do
begin
if l_comparer.Equals(x, y) then
begin
Result := true;
break;
end;
end;
end;
end.
To use include the class, then write if(TUtilityArray<integer>.Contains(some integer value, [value1, value2 etc.])) then .... An added benefit of this method is that it works for other primitives as well.
Is there a way to convert the two-letter Country Codes into their readable counterparts without using external ressources?
e.g. DE -> Germany, AD -> Andorra
It would be great if I could select the target language or it's using the system language, because I'd like to have them in German.
As #Uwe mentioned in his comment, you can use the EnumSystemGeoID and GetGeoInfo functions. The principle is that with EnumSystemGeoID function you'll enumerate geographical location identifiers and by the GetGeoInfo function query if the enumerated identifier's ISO 2-letter country / region code (info type GEO_ISO2) equals to the one of your interest. If so, then you can query for this identifier with the same function either a friendly name (info type GEO_FRIENDLYNAME), or the official name (info type GEO_OFFICIALNAME), return the result and stop the enumeration.
Here is an example code, which might do that (unfortunately, the enumeration function does not support passing custom data, so I've used a global record variable for passing values):
type
TEnumData = record
GeoCode: string;
GeoName: string;
Success: Boolean;
end;
GEOID = type LONG;
GEOTYPE = type DWORD;
GEOCLASS = type DWORD;
SYSGEOTYPE = (
GEO_NATION = $0001,
GEO_LATITUDE = $0002,
GEO_LONGITUDE = $0003,
GEO_ISO2 = $0004,
GEO_ISO3 = $0005,
GEO_RFC1766 = $0006,
GEO_LCID = $0007,
GEO_FRIENDLYNAME= $0008,
GEO_OFFICIALNAME= $0009,
GEO_TIMEZONES = $000A,
GEO_OFFICIALLANGUAGES = $000B,
GEO_ISO_UN_NUMBER = $000C,
GEO_PARENT = $000D
);
SYSGEOCLASS = (
GEOCLASS_NATION = 16,
GEOCLASS_REGION = 14,
GEOCLASS_ALL = 0
);
GEO_ENUMPROC = function(GeoId: GEOID): BOOL; stdcall;
function EnumSystemGeoID(GeoClass: GEOCLASS;
ParentGeoId: GEOID; lpGeoEnumProc: GEO_ENUMPROC): BOOL; stdcall;
external kernel32 name 'EnumSystemGeoID';
function GetGeoInfo(Location: GEOID; GeoType: GEOTYPE;
lpGeoData: LPTSTR; cchData: Integer; LangId: LANGID): Integer; stdcall;
external kernel32 name {$IFDEF UNICODE}'GetGeoInfoW'{$ELSE}'GetGeoInfoA'{$ENDIF};
implementation
var
// I have used this global variable due to a lack of user data parameter for the callback function
EnumData: TEnumData;
function TryGetGeoInfo(GeoId: GEOID; GeoType: GEOTYPE; out Value: string): Boolean;
var
Buffer: string;
BufferLen: Integer;
begin
Result := False;
BufferLen := GetGeoInfo(GeoId, GeoType, LPTSTR(Buffer), 0, 0);
if BufferLen <> 0 then
begin
SetLength(Buffer, BufferLen);
Result := GetGeoInfo(GeoId, GeoType, LPTSTR(Buffer), BufferLen, 0) <> 0;
if Result then
Value := Trim(Buffer);
end;
end;
function EnumGeoInfoProc(GeoId: GEOID): BOOL; stdcall;
var
S: string;
begin
Result := TryGetGeoInfo(GeoId, GEOTYPE(GEO_ISO2), S);
if Result and (S = EnumData.GeoCode) then
begin
// stop the enumeration since we've found the country by its ISO code
Result := False;
// return the success flag and try to return the friendly name of the country to the
// EnumData.GeoName record field; you can optionally query the GEO_OFFICIALNAME
EnumData.Success := TryGetGeoInfo(GeoId, GEOTYPE(GEO_FRIENDLYNAME), EnumData.GeoName);
end;
end;
function TryGetCountryNameByISO2(const Code: string; out Name: string): Boolean;
begin
// here is the brainless part using global record variable (because the function used
// here with its callback does not support passing user data); no, you cannot tune it
// up by making the callback function nested
EnumData.GeoCode := Code;
EnumData.Success := False;
if not EnumSystemGeoID(GEOCLASS(GEOCLASS_NATION), 0, EnumGeoInfoProc) then
RaiseLastOSError;
Result := EnumData.Success;
if Result then
Name := EnumData.GeoName;
end;
And a possible usage:
var
S: string;
begin
if TryGetCountryNameByISO2('DE', S) then
ShowMessage(S);
end;
You can iterate Languages (from Sysutils) and check the Ext property. The corresponding Name property will give you the localized language name.
for I := 0 to Languages.Count - 1 do begin
Writeln(Languages.Ext[I], '=', Languages.Name[I]);
end;
The sublanguage element suggested by Uwe Raabe is helping, but the results aren't that good, because It won't find all ISO codes and sometimes returns something different to a country name, like Simplified Chinese.
function _GetCountryFromISO(const aISO: string): string;
const
cStatement1 = '-(.*)';
cStatement2 = '\((.*?)\)';
var
i: Integer;
match: TMatch;
begin
Result := aISO; // default result if not found
for i := 0 to Languages.Count - 1 do begin
match := TRegEx.Match(Languages.LocaleName[i], cStatement1);
if not match.Success then
Assert(False, '');
if (aISO.Equals(match.Groups[1].Value)) then begin
match := TRegEx.Match(Languages.Name[i], cStatement2);
if not match.Success then
Assert(False, '');
Exit(match.Groups[1].Value);
end;
end;
end;
// Get a list of accounts in a domain separated by \x00 and ended by \x00\x00
Function GetUserList(AName: PAnsiChar; Var List; Size: Longint): Longint; StdCall;
I need to call the above from XE6.
Would someone be kind enough to post an example of how I can
get this buffer, and put it to a stream or a string.
The variable "List" is supposed to fill up some buffer, which I can read
off the list of users.
After trying for a couple of options, I have tried all options such as:
thanks!
var
Buffer: array of Byte;
iCount : Integer;
sName : AnsiString;
begin
...
SetLength(Buffer, 4096);
iCount := GetUserListTest(PAnsiChar(sName)#Buffer[0], Length(Buffer)); // cannot
// iCount := GetUserList(PAnsiChar(sName), Buffer, Length(Buffer));
That is not a Win32 API function, so it must be a third-party function. Ask the vendor for an example.
A var parameter expects you to pass a variable to it. The var receives the address of the variable. #Buffer[0] does not satisfy that requirement, as # returns a Pointer, and then the var ends up with the address of the pointer itself, not the address of the variable being pointed at. The function is expecting a pointer to a buffer. By using a var to receive that pointer, you need to drop the # and pass the first array element, so that the address of that element (effectively the address of the buffer) will be passed to the function, eg:
iCount := GetUserList(PAnsiChar(sName), Buffer[0], iCount);
Alternatively, you can use this syntax instead, which will pass the same address of the first element:
iCount := GetUserList(PAnsiChar(sName), PByte(Buffer)^, iCount);
Now, with that said, chances are that the function may allow you to query it for the necessary array size so you can allocate only what is actually needed (but check the documentation to be sure, I'm making an assumption here since you have not said otherwise)), eg:
procedure GetDomainUsers(const Domain: AnsiString; Users: TStrings);
var
Buffer: array of AnsiChar;
iCount : Integer;
User: PAnsiChar;
begin
// this call ASSUMES the function returns the needed
// bytecount when given a NULL/empty array - check
// the documentation!!!
iCount := GetUserList(PAnsiChar(Domain), PAnsiChar(nil)^, 0);
if iCount > 0 then
begin
SetLength(Buffer, iCount);
iCount := GetUserList(PAnsiChar(Domain), Buffer[0]{or: PAnsiChar(Buffer)^}, iCount);
end;
if iCount > 0 then
begin
Users.BeginUpdate;
try
User := PAnsiChar(Buffer);
while User^ <> #0 do
begin
Users.Add(User);
Inc(User, StrLen(User)+1);
end;
finally
Users.EndUpdate;
end;
end;
end;
If that does not work, then you will have to pre-allocate a large array:
procedure GetDomainUsers(const Domain: AnsiString; Users: TStrings);
var
Buffer: array of AnsiChar;
User: PAnsiChar;
begin
SetLength(Buffer, 1024);
if GetUserList(PAnsiChar(Domain), Buffer[0]{or: PAnsiChar(Buffer)^}, Length(Buffer)) > 0 then
begin
Users.BeginUpdate;
try
User := PAnsiChar(Buffer);
while User^ <> #0 do
begin
Users.Add(User);
Inc(User, StrLen(User)+1);
end;
finally
Users.EndUpdate;
end;
end;
end;
Earlier I ask how can I set Internet Proxy in Windows connection.
But my problem is that if I want to set all connections, then must be list connection names list. I search over net but I can't find any answer for this.
You're looking for RasEnumEntries function. Delphi doesn't have RAS API functions declared, but easy if you've got JVCL. The TJvRas32 component has a PhoneBook TStrings property in which available connections are populated.
Below is D2007 test code (no error checking):
const
RAS_MaxEntryName = 256;
RASBASE = 600;
ERROR_BUFFER_TOO_SMALL = RASBASE + 3;
ERROR_INVALID_SIZE = RASBASE + 32;
type
PRasEntryName = ^TRasEntryName;
TRasEntryName = record
dwSize: Longint;
szEntryName: array [0..RAS_MaxEntryName] of Char;
end;
function RasEnumEntriesA(reserved: PChar; lpszPhonebook: PChar;
lpRasEntryName: PRasEntryName; var lpcb: DWORD;
var lpcEntries: DWORD): DWORD; stdcall; external 'RASAPI32.DLL';
procedure GetRasEntries(List: TStrings);
var
RasEntryNames: array of TRasEntryName;
Err, Size, Entries: DWORD;
i: Integer;
begin
List.Clear;
SetLength(RasEntryNames, 1);
Size := SizeOf(TRasEntryName);
RasEntryNames[0].dwSize := Size;
Err := RasEnumEntriesA(nil, nil, #RasEntryNames[0], Size, Entries);
if (Err = ERROR_BUFFER_TOO_SMALL) and (Entries > 0) then begin
Assert(Size = SizeOf(TRasEntryName) * Entries);
SetLength(RasEntryNames, Entries);
Err := RasEnumEntriesA(nil, nil, #RasEntryNames[0], Size, Entries);
if Err = 0 then
for i := 0 to Length(RasEntryNames) do
List.Add(RasEntryNames[i].szEntryName);
end else
List.Add(RasEntryNames[0].szEntryName);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
GetRasEntries(ListBox1.Items);
end;
I'm looking for the best* method to find the primary email address for the currently logged in Active Directory user (using GetUserName to get the logged in username)
I have seen How do integrate Delphi with Active Directory? but I couldn't get this to work with Delphi 2010.
(*best method: the eventual application will be run by users who do not have administrative access to the machine)
Edit 1:
Reading up on this, it appears that the email or mail field is probably not the best way to go as it seems it might not be populated, therefore I'd need to use the multivalue field of proxyaddresses
The code below works for me. It is an extract of a class I use in production code. It didn't get the proxyAddresses but I added that and it seems to work, although I get only one alternative e-mail address, looking like smtp: g.trol#mydomain.com. I can't find an example with more that one address, so you may need to test what happens then.
Also, I tested this in Delphi 2007, using a type library I found somewhere, because I had trouble importing it. In the code you see __MIDL_0010, which is a __MIDL___MIDL_itf_ads_0000_0017 record property of the field value. I noticed this was named otherwise in a different version of the type library, so you may need to make some tweaks to this code to suit your exact type library import, an maybe fix some ansi/unicode differences.
uses ActiveX, ComObj, ActiveDs_TLB;
const
NETAPI32DLL = 'netapi32.dll';
const
ACTIVEDSDLL = 'activeds.dll';
ADS_SECURE_AUTHENTICATION = $00000001;
const
// ADSI success codes
S_ADS_ERRORSOCCURRED = $00005011;
S_ADS_NOMORE_ROWS = $00005012;
S_ADS_NOMORE_COLUMNS = $00005013;
// ADSI error codes
E_ADS_BAD_PATHNAME = $80005000;
E_ADS_INVALID_DOMAIN_OBJECT = $80005001;
E_ADS_INVALID_USER_OBJECT = $80005002;
E_ADS_INVALID_COMPUTER_OBJECT = $80005003;
E_ADS_UNKNOWN_OBJECT = $80005004;
E_ADS_PROPERTY_NOT_SET = $80005005;
E_ADS_PROPERTY_NOT_SUPPORTED = $80005006;
E_ADS_PROPERTY_INVALID = $80005007;
E_ADS_BAD_PARAMETER = $80005008;
E_ADS_OBJECT_UNBOUND = $80005009;
E_ADS_PROPERTY_NOT_MODIFIED = $8000500A;
E_ADS_PROPERTY_MODIFIED = $8000500B;
E_ADS_CANT_CONVERT_DATATYPE = $8000500C;
E_ADS_PROPERTY_NOT_FOUND = $8000500D;
E_ADS_OBJECT_EXISTS = $8000500E;
E_ADS_SCHEMA_VIOLATION = $8000500F;
E_ADS_COLUMN_NOT_SET = $80005010;
E_ADS_INVALID_FILTER = $80005014;
type
TNetWkstaGetInfo = function(ServerName: PWideChar; Level: Cardinal;
out BufPtr: Pointer): Cardinal; stdcall;
TADsOpenObject = function (lpszPathName: PWideChar; lpszUserName: PWideChar;
lpszPassword: PWideChar; dwReserved: DWORD; const riid: TGUID;
out pObject): HRESULT; stdcall;
TADsGetObject = function(PathName: PWideChar; const IID: TGUID; out Void):
HRESULT; stdcall;
var
NetLibHandle: THandle;
NetWkstaGetInfo : TNetWkstaGetInfo;
AdsLibHandle: THandle;
_ADsOpenObject : TADsOpenObject;
_ADsGetObject :TADsGetObject;
// VB-like GetObject function
function GetObject(const Name: String): IDispatch;
var
Moniker: IMoniker;
Eaten: integer;
BindContext: IBindCtx;
Dispatch: IDispatch;
begin
OleCheck(CreateBindCtx(0, BindContext));
OleCheck(MkParseDisplayName(BindContext,
PWideChar(WideString(Name)),
Eaten,
Moniker));
OleCheck(Moniker.BindToObject(BindContext, nil, IDispatch, Dispatch));
Result := Dispatch;
end;
// Some network info
type
PWkstaInfo100 = ^TWkstaInfo100;
_WKSTA_INFO_100 = record
wki100_platform_id: DWORD;
wki100_computername: LPWSTR;
wki100_langroup: LPWSTR;
wki100_ver_major: DWORD;
wki100_ver_minor: DWORD;
end;
TWkstaInfo100 = _WKSTA_INFO_100;
WKSTA_INFO_100 = _WKSTA_INFO_100;
function GetCurrentDomain: String;
var
pWI: PWkstaInfo100;
begin
if Win32Platform = VER_PLATFORM_WIN32_NT then
begin
if NetWkstaGetInfo(nil, 100, Pointer(pWI)) = 0 then
Result := String(pWI.wki100_langroup);
end;
end;
// ADs...Object function wrappers
function ADsGetObject(PathName: PWideChar; const IID: TGUID;
out Void): HRESULT;
begin
if Assigned(_ADsGetObject) then
Result := _ADsGetObject(PathName, IID, Void)
else
Result := ERROR_CALL_NOT_IMPLEMENTED;
end;
function ADsOpenObject(lpszPathName, lpszUserName,
lpszPassword: PWideChar; dwReserved: DWORD; const riid: TGUID;
out pObject): HRESULT;
begin
if Assigned(_ADsOpenObject) then
Result := _ADsOpenObject(lpszPathName, lpszUserName, lpszPassword, dwReserved, riid, pObject)
else
Result := ERROR_CALL_NOT_IMPLEMENTED;
end;
// The main function
function GetUserInfo(UserAccountName: string): Boolean;
var
// Domain info: Max password age
RootDSE: Variant;
Domain: Variant;
MaxPwdNanoAge: Variant;
MaxPasswordAge: Int64;
DNSDomain: String;
// User info: User directorysearch to find the user by username
DirectorySearch: IDirectorySearch;
SearchPreferences: array[0..1] of ADS_SEARCHPREF_INFO;
Columns: array[0..6] of PWideChar;
SearchResult: Cardinal;
hr: HRESULT;
ColumnResult: ads_search_column;
// Number of user records found
RecordCount: Integer;
LastSetDateTime: TDateTime;
ExpireDateTime: TDateTime;
i: Integer;
begin
Result := False;
// If no account name is set, reading is impossible. Return false.
if (UserAccountName = '') then
Exit;
try
// Read the maximum password age from the domain.
// To do: Check if this can be done with ADsGetObject instead of the VB-like GetObject
// Get the Root DSE.
RootDSE := GetObject('LDAP://RootDSE');
DNSDomain := RootDSE.Get('DefaultNamingContext');
Domain := GetObject('LDAP://' + DNSDomain);
// Build an array of user properties to receive.
Columns[0] := StringToOleStr('AdsPath');
Columns[1] := StringToOleStr('pwdLastSet');
Columns[2] := StringToOleStr('displayName');
Columns[3] := StringToOleStr('mail');
Columns[4] := StringToOleStr('sAMAccountName');
Columns[5] := StringToOleStr('userPrincipalName');
Columns[6] := StringToOleStr('proxyAddresses');
// Bind to the directorysearch object. For some unspecified reason, the regular
// domain name (yourdomain) needs to be used instead of the AdsPath (office.yourdomain.us)
AdsGetObject(PWideChar(WideString('LDAP://' + GetCurrentDomain)), IDirectorySearch, DirectorySearch);
try
// Set search preferences.
SearchPreferences[0].dwSearchPref := ADS_SEARCHPREF_SEARCH_SCOPE;
SearchPreferences[0].vValue.dwType := ADSTYPE_INTEGER;
SearchPreferences[0].vValue.__MIDL_0010.Integer := ADS_SCOPE_SUBTREE;
DirectorySearch.SetSearchPreference(#SearchPreferences[0], 1);
// Execute search
// Search for SAM account name (g.trol) and User Principal name
// (g.trol#yourdomain.com). This allows the user to enter their username
// in both ways. Add CN=* to filter out irrelevant objects that might
// match the principal name.
DirectorySearch.ExecuteSearch(
PWideChar(WideString(
Format('(&(CN=*)(|(sAMAccountName=%0:s)(userPrincipalName=%0:s)))',
[UserAccountName]))),
nil,
$FFFFFFFF,
SearchResult);
// Get records
RecordCount := 0;
hr := DirectorySearch.GetNextRow(SearchResult);
if (hr <> S_ADS_NOMORE_ROWS) then
begin
// 1 row found
Inc(RecordCount);
// Get the column values for this row.
// To do: This code could use a more general and neater approach!
for i := Low(Columns) to High(Columns) do
begin
hr := DirectorySearch.GetColumn(SearchResult, Columns[i], ColumnResult);
if Succeeded(hr) then
begin
// Get the values for the columns.
{if SameText(ColumnResult.pszAttrName, 'AdsPath') then
Result.UserAdsPath :=
ColumnResult.pADsValues.__MIDL_0010.CaseIgnoreString
else if SameText(ColumnResult.pszAttrName, 'pwdLastSet') then
begin
LastSetDateTime := LDapTimeStampToDateTime(
ColumnResult.pAdsvalues^.__MIDL_0010.LargeInteger) +
GetTimeZoneCorrection;
ExpireDateTime := IncMilliSecond(LastSetDateTime,
LDapIntervalToMSecs(MaxPasswordAge));
Result.UserPasswordExpireDateTime := ExpireDateTime;
end
else if SameText(ColumnResult.pszAttrName, 'displayName') then
Result.UserFullName := ColumnResult.pADsValues.__MIDL_0010.CaseIgnoreString
else if SameText(ColumnResult.pszAttrName, 'mail') then
Result.UserEmail := ColumnResult.pADsValues.__MIDL_0010.CaseIgnoreString
else if SameText(ColumnResult.pszAttrName, 'sAMAccountName') then
Result.UserShortAccountName := ColumnResult.pADsValues.__MIDL_0010.CaseIgnoreString
else if SameText(ColumnResult.pszAttrName, 'userPrincipalName') then
Result.UserFullAccountName := ColumnResult.pADsValues.__MIDL_0010.CaseIgnoreString
else ..}
if SameText(ColumnResult.pszAttrName, 'proxyAddresses') then
ShowMessage(ColumnResult.pADsValues.__MIDL_0010.CaseIgnoreString);
// Free the column result
DirectorySearch.FreeColumn(ColumnResult);
end;
end;
// Small check if this account indeed is the only one found.
// No need to check the exact number. <> 1 = error
Hr := DirectorySearch.GetNextRow(SearchResult);
if (hr <> S_ADS_NOMORE_ROWS) then
Inc(RecordCount);
end;
// Close the search
DirectorySearch.CloseSearchHandle(SearchResult);
// Exactly 1 record found?
if RecordCount = 1 then
Result := True
else
ShowMessageFmt('More than one account found when searching for %s in ' +
'Active Directory.', [UserAccountName]);
finally
DirectorySearch := nil;
end;
except
Result := False;
end;
end;
initialization
NetLibHandle := LoadLibrary(NETAPI32DLL);
if NetLibHandle <> 0 then
#NetWkstaGetInfo := GetProcAddress(NetLibHandle, 'NetWkstaGetInfo');
ADsLibHandle := LoadLibrary(ACTIVEDSDLL);
if ADsLibHandle <> 0 then
begin
#_ADsOpenObject := GetProcAddress(ADsLibHandle, 'ADsOpenObject');
#_ADsGetObject := GetProcAddress(ADsLibHandle, 'ADsGetObject');
end;
finalization
FreeLibrary(ADsLibHandle);
FreeLibrary(NetLibHandle);
end.
Call like this:
GetUserInfo('g.trol' {or g.trol#yourdomain.com});
Download from My dropbox