mirror of
https://github.com/reactos/reactos.git
synced 2025-07-04 19:51:21 +00:00
Nameservers are now correctly listed when per adapter nameservers are
specified in the registry. This now does the same thing I observe on win2k, GetNetworkParams. This function is used by our DNS query implementation to find out what nameservers it should use. There is a test case in apps/tests/nameserverlist. svn path=/trunk/; revision=7282
This commit is contained in:
parent
4aa9677fa4
commit
ff637e689a
5 changed files with 310 additions and 7 deletions
|
@ -20,6 +20,8 @@
|
||||||
#include <iphlpapi.h>
|
#include <iphlpapi.h>
|
||||||
#include <icmpapi.h>
|
#include <icmpapi.h>
|
||||||
|
|
||||||
|
#include "ipprivate.h"
|
||||||
|
#include "ipregprivate.h"
|
||||||
#include "debug.h"
|
#include "debug.h"
|
||||||
//#include "trace.h"
|
//#include "trace.h"
|
||||||
|
|
||||||
|
@ -142,7 +144,8 @@ GetAdaptersInfo(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen)
|
||||||
return ERROR_INVALID_PARAMETER;
|
return ERROR_INVALID_PARAMETER;
|
||||||
ZeroMemory(pAdapterInfo, *pOutBufLen);
|
ZeroMemory(pAdapterInfo, *pOutBufLen);
|
||||||
|
|
||||||
lErr = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Adapters", 0, KEY_READ, &hAdapters);
|
lErr = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||||
|
L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Adapters", 0, KEY_READ, &hAdapters);
|
||||||
if(lErr != ERROR_SUCCESS)
|
if(lErr != ERROR_SUCCESS)
|
||||||
return lErr;
|
return lErr;
|
||||||
|
|
||||||
|
@ -309,7 +312,7 @@ GetInterfaceInfo(PIP_INTERFACE_INFO pIfTable, PULONG pOutBufLen)
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
if ((errCode = GetNumberOfInterfaces(&dwNumIf)) != NO_ERROR) {
|
if ((errCode = GetNumberOfInterfaces(&dwNumIf)) != NO_ERROR) {
|
||||||
_tprintf(_T("GetInterfaceInfo() failed with code 0x%08X - Use FormatMessage to obtain the message string for the returned error\n"), errCode);
|
_tprintf(_T("GetInterfaceInfo() failed with code 0x%08X - Use FormatMessage to obtain the message string for the returned error\n"), (int)errCode);
|
||||||
return errCode;
|
return errCode;
|
||||||
}
|
}
|
||||||
if (dwNumIf == 0) return ERROR_NO_DATA; // No adapter information exists for the local computer
|
if (dwNumIf == 0) return ERROR_NO_DATA; // No adapter information exists for the local computer
|
||||||
|
@ -347,6 +350,123 @@ GetInterfaceInfo(PIP_INTERFACE_INFO pIfTable, PULONG pOutBufLen)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* EnumNameServers
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void EnumNameServers( HANDLE RegHandle, PWCHAR Interface,
|
||||||
|
PVOID Data, EnumNameServersFunc cb ) {
|
||||||
|
PWCHAR NameServerString = QueryRegistryValueString(RegHandle, L"NameServer");
|
||||||
|
/* Now, count the non-empty comma separated */
|
||||||
|
if (NameServerString) {
|
||||||
|
DWORD ch;
|
||||||
|
DWORD LastNameStart = 0;
|
||||||
|
for (ch = 0; NameServerString[ch]; ch++) {
|
||||||
|
if (NameServerString[ch] == ',') {
|
||||||
|
if (ch - LastNameStart > 0) { /* Skip empty entries */
|
||||||
|
PWCHAR NameServer = malloc(sizeof(WCHAR) * (ch - LastNameStart + 1));
|
||||||
|
if (NameServer) {
|
||||||
|
memcpy(NameServer,NameServerString + LastNameStart,
|
||||||
|
(ch - LastNameStart) * sizeof(WCHAR));
|
||||||
|
NameServer[ch - LastNameStart] = 0;
|
||||||
|
cb( Interface, NameServer, Data );
|
||||||
|
free(NameServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LastNameStart = ch + 1; /* The first one after the comma */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ch - LastNameStart > 0) { /* A last name? */
|
||||||
|
PWCHAR NameServer = malloc(sizeof(WCHAR) * (ch - LastNameStart + 1));
|
||||||
|
memcpy(NameServer,NameServerString + LastNameStart,
|
||||||
|
(ch - LastNameStart) * sizeof(WCHAR));
|
||||||
|
NameServer[ch - LastNameStart] = 0;
|
||||||
|
cb( Interface, NameServer, Data );
|
||||||
|
free(NameServer);
|
||||||
|
}
|
||||||
|
ConsumeRegValueString(NameServerString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* EnumInterfaces
|
||||||
|
*
|
||||||
|
* Call the enumeration function for each name server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void EnumInterfaces( PVOID Data, EnumInterfacesFunc cb ) {
|
||||||
|
HANDLE RegHandle;
|
||||||
|
HANDLE ChildKeyHandle = 0;
|
||||||
|
PWCHAR RegKeyToEnumerate =
|
||||||
|
L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces";
|
||||||
|
PWCHAR ChildKeyName = 0;
|
||||||
|
DWORD CurrentInterface;
|
||||||
|
|
||||||
|
if (OpenChildKeyRead(HKEY_LOCAL_MACHINE,RegKeyToEnumerate,&RegHandle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CurrentInterface = 0; TRUE; CurrentInterface++) {
|
||||||
|
ChildKeyName = GetNthChildKeyName( RegHandle, CurrentInterface );
|
||||||
|
if (!ChildKeyName) break;
|
||||||
|
if (OpenChildKeyRead(RegHandle,ChildKeyName,
|
||||||
|
&ChildKeyHandle) == 0) {
|
||||||
|
cb( ChildKeyHandle, ChildKeyName, Data );
|
||||||
|
RegCloseKey( ChildKeyHandle );
|
||||||
|
}
|
||||||
|
ConsumeChildKeyName( ChildKeyName );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CreateNameServerListEnumNamesFuncCount( PWCHAR Interface,
|
||||||
|
PWCHAR Server,
|
||||||
|
PVOID _Data ) {
|
||||||
|
PNAME_SERVER_LIST_PRIVATE Data = (PNAME_SERVER_LIST_PRIVATE)_Data;
|
||||||
|
Data->NumServers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CreateNameServerListEnumIfFuncCount( HANDLE RegHandle,
|
||||||
|
PWCHAR InterfaceName,
|
||||||
|
PVOID _Data ) {
|
||||||
|
PNAME_SERVER_LIST_PRIVATE Data = (PNAME_SERVER_LIST_PRIVATE)_Data;
|
||||||
|
EnumNameServers(RegHandle,InterfaceName,Data,
|
||||||
|
CreateNameServerListEnumNamesFuncCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CreateNameServerListEnumNamesFunc( PWCHAR Interface,
|
||||||
|
PWCHAR Server,
|
||||||
|
PVOID _Data ) {
|
||||||
|
PNAME_SERVER_LIST_PRIVATE Data = (PNAME_SERVER_LIST_PRIVATE)_Data;
|
||||||
|
wcstombs(Data->AddrString[Data->CurrentName].IpAddress.String,
|
||||||
|
Server,
|
||||||
|
sizeof(IP_ADDRESS_STRING));
|
||||||
|
strcpy(Data->AddrString[Data->CurrentName].IpMask.String,"0.0.0.0");
|
||||||
|
Data->AddrString[Data->CurrentName].Context = 0;
|
||||||
|
if (Data->CurrentName < Data->NumServers - 1) {
|
||||||
|
Data->AddrString[Data->CurrentName].Next =
|
||||||
|
&Data->AddrString[Data->CurrentName+1];
|
||||||
|
} else
|
||||||
|
Data->AddrString[Data->CurrentName].Next = 0;
|
||||||
|
|
||||||
|
Data->CurrentName++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CreateNameServerListEnumIfFunc( HANDLE RegHandle,
|
||||||
|
PWCHAR InterfaceName,
|
||||||
|
PVOID _Data ) {
|
||||||
|
PNAME_SERVER_LIST_PRIVATE Data = (PNAME_SERVER_LIST_PRIVATE)_Data;
|
||||||
|
EnumNameServers(RegHandle,InterfaceName,Data,
|
||||||
|
CreateNameServerListEnumNamesFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int CountNameServers( PNAME_SERVER_LIST_PRIVATE PrivateData ) {
|
||||||
|
EnumInterfaces(PrivateData,CreateNameServerListEnumIfFuncCount);
|
||||||
|
return PrivateData->NumServers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MakeNameServerList( PNAME_SERVER_LIST_PRIVATE PrivateData ) {
|
||||||
|
EnumInterfaces(PrivateData,CreateNameServerListEnumIfFunc);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @implemented
|
* @implemented
|
||||||
|
@ -359,17 +479,23 @@ GetNetworkParams(PFIXED_INFO pFixedInfo, PULONG pOutBufLen)
|
||||||
DWORD dwSize;
|
DWORD dwSize;
|
||||||
HKEY hKey;
|
HKEY hKey;
|
||||||
LONG errCode;
|
LONG errCode;
|
||||||
|
NAME_SERVER_LIST_PRIVATE PrivateNSEnum = { 0 };
|
||||||
|
|
||||||
if (pFixedInfo == NULL || pOutBufLen == NULL) return ERROR_INVALID_PARAMETER;
|
CountNameServers( &PrivateNSEnum );
|
||||||
|
|
||||||
|
if (pOutBufLen == NULL) return ERROR_INVALID_PARAMETER;
|
||||||
|
|
||||||
if (*pOutBufLen < sizeof(FIXED_INFO))
|
if (*pOutBufLen < sizeof(FIXED_INFO))
|
||||||
{
|
{
|
||||||
*pOutBufLen = sizeof(FIXED_INFO);
|
*pOutBufLen = sizeof(FIXED_INFO) +
|
||||||
|
((PrivateNSEnum.NumServers - 1) * sizeof(IP_ADDR_STRING));
|
||||||
return ERROR_BUFFER_OVERFLOW;
|
return ERROR_BUFFER_OVERFLOW;
|
||||||
}
|
}
|
||||||
|
if (pFixedInfo == NULL) return ERROR_INVALID_PARAMETER;
|
||||||
memset(pFixedInfo, 0, sizeof(FIXED_INFO));
|
memset(pFixedInfo, 0, sizeof(FIXED_INFO));
|
||||||
|
|
||||||
errCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters", 0, KEY_READ, &hKey);
|
errCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||||
|
L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters", 0, KEY_READ, &hKey);
|
||||||
if (errCode == ERROR_SUCCESS)
|
if (errCode == ERROR_SUCCESS)
|
||||||
{
|
{
|
||||||
dwSize = sizeof(pFixedInfo->HostName);
|
dwSize = sizeof(pFixedInfo->HostName);
|
||||||
|
@ -384,6 +510,51 @@ GetNetworkParams(PFIXED_INFO pFixedInfo, PULONG pOutBufLen)
|
||||||
dwSize = sizeof(pFixedInfo->EnableRouting);
|
dwSize = sizeof(pFixedInfo->EnableRouting);
|
||||||
errCode = RegQueryValueExW(hKey, L"IPEnableRouter", NULL, NULL, (LPBYTE)&pFixedInfo->EnableRouting, &dwSize);
|
errCode = RegQueryValueExW(hKey, L"IPEnableRouter", NULL, NULL, (LPBYTE)&pFixedInfo->EnableRouting, &dwSize);
|
||||||
RegCloseKey(hKey);
|
RegCloseKey(hKey);
|
||||||
|
|
||||||
|
/* Get the number of name servers */
|
||||||
|
PIP_ADDR_STRING AddressAfterFixedInfo;
|
||||||
|
AddressAfterFixedInfo = (PIP_ADDR_STRING)&pFixedInfo[1];
|
||||||
|
DWORD NumberOfServersAllowed = 0, CurrentServer = 0;
|
||||||
|
|
||||||
|
while( &AddressAfterFixedInfo[NumberOfServersAllowed] <
|
||||||
|
(PIP_ADDR_STRING)(((PCHAR)pFixedInfo) + *pOutBufLen) )
|
||||||
|
NumberOfServersAllowed++;
|
||||||
|
|
||||||
|
NumberOfServersAllowed++; /* One struct is built in */
|
||||||
|
|
||||||
|
/* Since the first part of the struct is built in, we have to do some
|
||||||
|
fiddling */
|
||||||
|
PrivateNSEnum.AddrString =
|
||||||
|
malloc(NumberOfServersAllowed * sizeof(IP_ADDR_STRING));
|
||||||
|
if (PrivateNSEnum.NumServers > NumberOfServersAllowed)
|
||||||
|
PrivateNSEnum.NumServers = NumberOfServersAllowed;
|
||||||
|
MakeNameServerList( &PrivateNSEnum );
|
||||||
|
|
||||||
|
/* Now we have the name servers, place the first one in the struct,
|
||||||
|
and follow it with the rest */
|
||||||
|
if (!PrivateNSEnum.NumServers)
|
||||||
|
RtlZeroMemory( &pFixedInfo->DnsServerList, sizeof(IP_ADDR_STRING) );
|
||||||
|
else
|
||||||
|
memcpy( &pFixedInfo->DnsServerList, &PrivateNSEnum.AddrString[0],
|
||||||
|
sizeof(PrivateNSEnum.AddrString[0]) );
|
||||||
|
pFixedInfo->CurrentDnsServer = &pFixedInfo->DnsServerList;
|
||||||
|
if (PrivateNSEnum.NumServers > 1)
|
||||||
|
memcpy( &AddressAfterFixedInfo[0],
|
||||||
|
&PrivateNSEnum.AddrString[1],
|
||||||
|
sizeof(IP_ADDR_STRING) * NumberOfServersAllowed - 1 );
|
||||||
|
else
|
||||||
|
pFixedInfo->CurrentDnsServer->Next = 0;
|
||||||
|
|
||||||
|
for( CurrentServer = 0;
|
||||||
|
CurrentServer < PrivateNSEnum.NumServers - 1;
|
||||||
|
CurrentServer++ ) {
|
||||||
|
pFixedInfo->CurrentDnsServer->Next = &AddressAfterFixedInfo[CurrentServer];
|
||||||
|
pFixedInfo->CurrentDnsServer = &AddressAfterFixedInfo[CurrentServer];
|
||||||
|
pFixedInfo->CurrentDnsServer->Next = 0;
|
||||||
|
}
|
||||||
|
/* For now, set the first server as the current server */
|
||||||
|
pFixedInfo->CurrentDnsServer = &pFixedInfo->DnsServerList;
|
||||||
|
free(PrivateNSEnum.AddrString);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
15
reactos/lib/iphlpapi/ipprivate.h
Normal file
15
reactos/lib/iphlpapi/ipprivate.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#ifndef IPPRIVATE_H
|
||||||
|
#define IPPRIVATE_H
|
||||||
|
|
||||||
|
typedef void (*EnumInterfacesFunc)( HANDLE RegHandle, PWCHAR InterfaceName,
|
||||||
|
PVOID Data );
|
||||||
|
typedef void (*EnumNameServersFunc)( PWCHAR InterfaceName, PWCHAR Server,
|
||||||
|
PVOID Data );
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int NumServers;
|
||||||
|
int CurrentName;
|
||||||
|
PIP_ADDR_STRING AddrString;
|
||||||
|
} NAME_SERVER_LIST_PRIVATE, *PNAME_SERVER_LIST_PRIVATE;
|
||||||
|
|
||||||
|
#endif/*IPPRIVATE_H*/
|
13
reactos/lib/iphlpapi/ipregprivate.h
Normal file
13
reactos/lib/iphlpapi/ipregprivate.h
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#ifndef IPREGPRIVATE_H
|
||||||
|
#define IPREGPRIVATE_H
|
||||||
|
|
||||||
|
int GetLongestChildKeyName( HANDLE RegHandle );
|
||||||
|
LONG OpenChildKeyRead( HANDLE RegHandle,
|
||||||
|
PWCHAR ChildKeyName,
|
||||||
|
HANDLE *ReturnHandle );
|
||||||
|
PWCHAR GetNthChildKeyName( HANDLE RegHandle, DWORD n );
|
||||||
|
void ConsumeChildKeyName( PWCHAR Name );
|
||||||
|
PWCHAR QueryRegistryValueString( HANDLE RegHandle, PWCHAR ValueName );
|
||||||
|
void ConsumeRegValueString( PWCHAR NameServer );
|
||||||
|
|
||||||
|
#endif/*IPREGPRIVATE_H*/
|
|
@ -11,11 +11,11 @@ TARGET_BASE = 0x777c0000
|
||||||
TARGET_CFLAGS = -DUNICODE -D_UNICODE
|
TARGET_CFLAGS = -DUNICODE -D_UNICODE
|
||||||
|
|
||||||
# require os code to explicitly request A/W version of structs/functions
|
# require os code to explicitly request A/W version of structs/functions
|
||||||
TARGET_CFLAGS += -D_DISABLE_TIDENTS -Wall -Werror
|
TARGET_CFLAGS += -D_DISABLE_TIDENTS -Wall -Werror -g
|
||||||
|
|
||||||
TARGET_SDKLIBS = ntdll.a kernel32.a
|
TARGET_SDKLIBS = ntdll.a kernel32.a
|
||||||
|
|
||||||
TARGET_OBJECTS = $(TARGET_NAME).o
|
TARGET_OBJECTS = $(TARGET_NAME).o registry.o
|
||||||
|
|
||||||
DEP_OBJECTS = $(TARGET_OBJECTS)
|
DEP_OBJECTS = $(TARGET_OBJECTS)
|
||||||
|
|
||||||
|
|
104
reactos/lib/iphlpapi/registry.c
Normal file
104
reactos/lib/iphlpapi/registry.c
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <tchar.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "ipregprivate.h"
|
||||||
|
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
int GetLongestChildKeyName( HANDLE RegHandle ) {
|
||||||
|
LONG Status;
|
||||||
|
DWORD MaxAdapterName;
|
||||||
|
|
||||||
|
Status = RegQueryInfoKeyW(RegHandle,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&MaxAdapterName,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL);
|
||||||
|
if (Status == STATUS_SUCCESS)
|
||||||
|
return MaxAdapterName + 1;
|
||||||
|
else
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LONG OpenChildKeyRead( HANDLE RegHandle,
|
||||||
|
PWCHAR ChildKeyName,
|
||||||
|
HANDLE *ReturnHandle ) {
|
||||||
|
return RegOpenKeyExW( RegHandle,
|
||||||
|
ChildKeyName,
|
||||||
|
0,
|
||||||
|
KEY_READ,
|
||||||
|
ReturnHandle );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Yields a malloced value that must be freed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PWCHAR GetNthChildKeyName( HANDLE RegHandle, DWORD n ) {
|
||||||
|
LONG Status;
|
||||||
|
int MaxAdapterName = GetLongestChildKeyName( RegHandle );
|
||||||
|
PWCHAR Value;
|
||||||
|
DWORD ValueLen;
|
||||||
|
|
||||||
|
if (MaxAdapterName == -1) {
|
||||||
|
RegCloseKey( RegHandle );
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueLen = MaxAdapterName;
|
||||||
|
Value = (PWCHAR)malloc( MaxAdapterName * sizeof(WCHAR) );
|
||||||
|
Status = RegEnumKeyExW( RegHandle, n, Value, &ValueLen,
|
||||||
|
NULL, NULL, NULL, NULL );
|
||||||
|
if (Status != STATUS_SUCCESS)
|
||||||
|
return 0;
|
||||||
|
else {
|
||||||
|
Value[ValueLen] = 0;
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsumeChildKeyName( PWCHAR Name ) {
|
||||||
|
if (Name) free( Name );
|
||||||
|
}
|
||||||
|
|
||||||
|
PWCHAR QueryRegistryValueString( HANDLE RegHandle, PWCHAR ValueName ) {
|
||||||
|
PWCHAR Name;
|
||||||
|
DWORD ReturnedSize = 0;
|
||||||
|
|
||||||
|
if (RegQueryValueExW( RegHandle, ValueName, NULL, NULL, NULL,
|
||||||
|
&ReturnedSize ) != 0)
|
||||||
|
return 0;
|
||||||
|
else {
|
||||||
|
Name = malloc( (ReturnedSize + 1) * sizeof(WCHAR) );
|
||||||
|
RegQueryValueExW( RegHandle, ValueName, NULL, NULL, (PVOID)Name,
|
||||||
|
&ReturnedSize );
|
||||||
|
Name[ReturnedSize] = 0;
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsumeRegValueString( PWCHAR Value ) {
|
||||||
|
if (Value) free(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
PWCHAR *QueryRegistryValueStringMulti( HANDLE RegHandle, PWCHAR ValueName ) {
|
||||||
|
return 0; /* FIXME if needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsumeRegValueStringMulti( PWCHAR *Value ) {
|
||||||
|
PWCHAR *Orig = Value;
|
||||||
|
if (Value) {
|
||||||
|
while (*Value) {
|
||||||
|
free(*Value);
|
||||||
|
}
|
||||||
|
free(Orig);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue