Home | Info | Community | Development | myReactOS | Contact Us
ReactOS Development > Doxygenadapter.c
Go to the documentation of this file.
00001 #include "rosdhcp.h" 00002 00003 SOCKET DhcpSocket = INVALID_SOCKET; 00004 static LIST_ENTRY AdapterList; 00005 static WSADATA wsd; 00006 00007 PCHAR *GetSubkeyNames( PCHAR MainKeyName, PCHAR Append ) { 00008 int i = 0; 00009 DWORD Error; 00010 HKEY MainKey; 00011 PCHAR *Out, OutKeyName; 00012 DWORD CharTotal = 0, AppendLen = 1 + strlen(Append); 00013 DWORD MaxSubKeyLen = 0, MaxSubKeys = 0; 00014 00015 Error = RegOpenKey( HKEY_LOCAL_MACHINE, MainKeyName, &MainKey ); 00016 00017 if( Error ) return NULL; 00018 00019 Error = RegQueryInfoKey 00020 ( MainKey, 00021 NULL, NULL, NULL, 00022 &MaxSubKeys, &MaxSubKeyLen, 00023 NULL, NULL, NULL, NULL, NULL, NULL ); 00024 00025 DH_DbgPrint(MID_TRACE,("MaxSubKeys: %d, MaxSubKeyLen %d\n", 00026 MaxSubKeys, MaxSubKeyLen)); 00027 00028 CharTotal = (sizeof(PCHAR) + MaxSubKeyLen + AppendLen) * (MaxSubKeys + 1); 00029 00030 DH_DbgPrint(MID_TRACE,("AppendLen: %d, CharTotal: %d\n", 00031 AppendLen, CharTotal)); 00032 00033 Out = (CHAR**) malloc( CharTotal ); 00034 OutKeyName = ((PCHAR)&Out[MaxSubKeys+1]); 00035 00036 if( !Out ) { RegCloseKey( MainKey ); return NULL; } 00037 00038 i = 0; 00039 do { 00040 Out[i] = OutKeyName; 00041 Error = RegEnumKey( MainKey, i, OutKeyName, MaxSubKeyLen ); 00042 if( !Error ) { 00043 strcat( OutKeyName, Append ); 00044 DH_DbgPrint(MID_TRACE,("[%d]: %s\n", i, OutKeyName)); 00045 OutKeyName += strlen(OutKeyName) + 1; 00046 i++; 00047 } else Out[i] = 0; 00048 } while( Error == ERROR_SUCCESS ); 00049 00050 RegCloseKey( MainKey ); 00051 00052 return Out; 00053 } 00054 00055 PCHAR RegReadString( HKEY Root, PCHAR Subkey, PCHAR Value ) { 00056 PCHAR SubOut = NULL; 00057 DWORD SubOutLen = 0, Error = 0; 00058 HKEY ValueKey = NULL; 00059 00060 DH_DbgPrint(MID_TRACE,("Looking in %x:%s:%s\n", Root, Subkey, Value )); 00061 00062 if( Subkey && strlen(Subkey) ) { 00063 if( RegOpenKey( Root, Subkey, &ValueKey ) != ERROR_SUCCESS ) 00064 goto regerror; 00065 } else ValueKey = Root; 00066 00067 DH_DbgPrint(MID_TRACE,("Got Key %x\n", ValueKey)); 00068 00069 if( (Error = RegQueryValueEx( ValueKey, Value, NULL, NULL, 00070 (LPBYTE)SubOut, &SubOutLen )) != ERROR_SUCCESS ) 00071 goto regerror; 00072 00073 DH_DbgPrint(MID_TRACE,("Value %s has size %d\n", Value, SubOutLen)); 00074 00075 if( !(SubOut = (CHAR*) malloc(SubOutLen)) ) 00076 goto regerror; 00077 00078 if( (Error = RegQueryValueEx( ValueKey, Value, NULL, NULL, 00079 (LPBYTE)SubOut, &SubOutLen )) != ERROR_SUCCESS ) 00080 goto regerror; 00081 00082 DH_DbgPrint(MID_TRACE,("Value %s is %s\n", Value, SubOut)); 00083 00084 goto cleanup; 00085 00086 regerror: 00087 if( SubOut ) { free( SubOut ); SubOut = NULL; } 00088 cleanup: 00089 if( ValueKey && ValueKey != Root ) { 00090 DH_DbgPrint(MID_TRACE,("Closing key %x\n", ValueKey)); 00091 RegCloseKey( ValueKey ); 00092 } 00093 00094 DH_DbgPrint(MID_TRACE,("Returning %x with error %d\n", SubOut, Error)); 00095 00096 return SubOut; 00097 } 00098 00099 HKEY FindAdapterKey( PDHCP_ADAPTER Adapter ) { 00100 int i = 0; 00101 PCHAR EnumKeyName = 00102 "SYSTEM\\CurrentControlSet\\Control\\Class\\" 00103 "{4D36E972-E325-11CE-BFC1-08002BE10318}"; 00104 PCHAR TargetKeyNameStart = 00105 "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\"; 00106 PCHAR TargetKeyName = NULL; 00107 PCHAR *EnumKeysLinkage = GetSubkeyNames( EnumKeyName, "\\Linkage" ); 00108 PCHAR *EnumKeysTop = GetSubkeyNames( EnumKeyName, "" ); 00109 PCHAR RootDevice = NULL; 00110 HKEY EnumKey, OutKey = NULL; 00111 DWORD Error = ERROR_SUCCESS; 00112 00113 if( !EnumKeysLinkage || !EnumKeysTop ) goto cleanup; 00114 00115 Error = RegOpenKey( HKEY_LOCAL_MACHINE, EnumKeyName, &EnumKey ); 00116 00117 if( Error ) goto cleanup; 00118 00119 for( i = 0; EnumKeysLinkage[i]; i++ ) { 00120 RootDevice = RegReadString 00121 ( EnumKey, EnumKeysLinkage[i], "RootDevice" ); 00122 00123 if( RootDevice && 00124 !strcmp( RootDevice, Adapter->DhclientInfo.name ) ) { 00125 TargetKeyName = 00126 (CHAR*) malloc( strlen( TargetKeyNameStart ) + 00127 strlen( RootDevice ) + 1); 00128 if( !TargetKeyName ) goto cleanup; 00129 sprintf( TargetKeyName, "%s%s", 00130 TargetKeyNameStart, RootDevice ); 00131 Error = RegCreateKeyExA( HKEY_LOCAL_MACHINE, TargetKeyName, 0, NULL, 0, KEY_READ, NULL, &OutKey, NULL ); 00132 break; 00133 } else { 00134 free( RootDevice ); RootDevice = 0; 00135 } 00136 } 00137 00138 cleanup: 00139 if( RootDevice ) free( RootDevice ); 00140 if( EnumKeysLinkage ) free( EnumKeysLinkage ); 00141 if( EnumKeysTop ) free( EnumKeysTop ); 00142 if( TargetKeyName ) free( TargetKeyName ); 00143 00144 return OutKey; 00145 } 00146 00147 BOOL PrepareAdapterForService( PDHCP_ADAPTER Adapter ) { 00148 HKEY AdapterKey; 00149 DWORD Error = ERROR_SUCCESS, DhcpEnabled, Length = sizeof(DWORD); 00150 00151 Adapter->DhclientState.config = &Adapter->DhclientConfig; 00152 strncpy(Adapter->DhclientInfo.name, (char*)Adapter->IfMib.bDescr, 00153 sizeof(Adapter->DhclientInfo.name)); 00154 00155 AdapterKey = FindAdapterKey( Adapter ); 00156 if( AdapterKey ) 00157 { 00158 Error = RegQueryValueEx(AdapterKey, "EnableDHCP", NULL, NULL, (LPBYTE)&DhcpEnabled, &Length); 00159 00160 if (Error != ERROR_SUCCESS || Length != sizeof(DWORD)) 00161 DhcpEnabled = 1; 00162 00163 CloseHandle(AdapterKey); 00164 } 00165 else 00166 { 00167 /* DHCP enabled by default */ 00168 DhcpEnabled = 1; 00169 } 00170 00171 if( !DhcpEnabled ) { 00172 /* Non-automatic case */ 00173 DbgPrint("DHCPCSVC: Adapter Name: [%s] (static)\n", Adapter->DhclientInfo.name); 00174 00175 Adapter->DhclientState.state = S_STATIC; 00176 } else { 00177 /* Automatic case */ 00178 DbgPrint("DHCPCSVC: Adapter Name: [%s] (dynamic)\n", Adapter->DhclientInfo.name); 00179 00180 Adapter->DhclientInfo.client->state = S_INIT; 00181 } 00182 00183 return TRUE; 00184 } 00185 00186 void AdapterInit() { 00187 WSAStartup(0x0101,&wsd); 00188 00189 InitializeListHead( &AdapterList ); 00190 } 00191 00192 int 00193 InterfaceConnected(const MIB_IFROW* IfEntry) 00194 { 00195 if (IfEntry->dwOperStatus == IF_OPER_STATUS_CONNECTED || 00196 IfEntry->dwOperStatus == IF_OPER_STATUS_OPERATIONAL) 00197 return 1; 00198 00199 DH_DbgPrint(MID_TRACE,("Interface %d is down\n", IfEntry->dwIndex)); 00200 return 0; 00201 } 00202 00203 BOOL 00204 IsReconnectHackNeeded(PDHCP_ADAPTER Adapter, const MIB_IFROW* IfEntry) 00205 { 00206 struct protocol *proto; 00207 PIP_ADAPTER_INFO AdapterInfo, Orig; 00208 DWORD Size, Ret; 00209 char *ZeroAddress = "0.0.0.0"; 00210 00211 proto = find_protocol_by_adapter(&Adapter->DhclientInfo); 00212 00213 if (Adapter->DhclientInfo.client->state == S_BOUND && !proto) 00214 return FALSE; 00215 00216 if (Adapter->DhclientInfo.client->state != S_BOUND && 00217 Adapter->DhclientInfo.client->state != S_STATIC) 00218 return FALSE; 00219 00220 ApiUnlock(); 00221 00222 Orig = AdapterInfo = HeapAlloc(GetProcessHeap(), 0, sizeof(IP_ADAPTER_INFO)); 00223 Size = sizeof(IP_ADAPTER_INFO); 00224 if (!AdapterInfo) 00225 { 00226 ApiLock(); 00227 return FALSE; 00228 } 00229 00230 Ret = GetAdaptersInfo(AdapterInfo, &Size); 00231 if (Ret == ERROR_BUFFER_OVERFLOW) 00232 { 00233 HeapFree(GetProcessHeap(), 0, AdapterInfo); 00234 AdapterInfo = HeapAlloc(GetProcessHeap(), 0, Size); 00235 if (!AdapterInfo) 00236 { 00237 ApiLock(); 00238 return FALSE; 00239 } 00240 00241 if (GetAdaptersInfo(AdapterInfo, &Size) != NO_ERROR) 00242 { 00243 ApiLock(); 00244 return FALSE; 00245 } 00246 00247 Orig = AdapterInfo; 00248 for (; AdapterInfo != NULL; AdapterInfo = AdapterInfo->Next) 00249 { 00250 if (AdapterInfo->Index == IfEntry->dwIndex) 00251 break; 00252 } 00253 00254 if (AdapterInfo == NULL) 00255 { 00256 HeapFree(GetProcessHeap(), 0, Orig); 00257 ApiLock(); 00258 return FALSE; 00259 } 00260 } 00261 else if (Ret != NO_ERROR) 00262 { 00263 HeapFree(GetProcessHeap(), 0, Orig); 00264 ApiLock(); 00265 return FALSE; 00266 } 00267 00268 if (!strcmp(AdapterInfo->IpAddressList.IpAddress.String, ZeroAddress)) 00269 { 00270 HeapFree(GetProcessHeap(), 0, Orig); 00271 ApiLock(); 00272 return TRUE; 00273 } 00274 else 00275 { 00276 HeapFree(GetProcessHeap(), 0, Orig); 00277 ApiLock(); 00278 return FALSE; 00279 } 00280 } 00281 00282 /* 00283 * XXX Figure out the way to bind a specific adapter to a socket. 00284 */ 00285 DWORD WINAPI AdapterDiscoveryThread(LPVOID Context) { 00286 PMIB_IFTABLE Table = (PMIB_IFTABLE) malloc(sizeof(MIB_IFTABLE)); 00287 DWORD Error, Size = sizeof(MIB_IFTABLE); 00288 PDHCP_ADAPTER Adapter = NULL; 00289 HANDLE AdapterStateChangedEvent = (HANDLE)Context; 00290 struct interface_info *ifi = NULL; 00291 struct protocol *proto; 00292 int i, AdapterCount = 0, Broadcast; 00293 00294 /* FIXME: Kill this thread when the service is stopped */ 00295 00296 do { 00297 DH_DbgPrint(MID_TRACE,("Getting Adapter List...\n")); 00298 00299 while( (Error = GetIfTable(Table, &Size, 0 )) == 00300 ERROR_INSUFFICIENT_BUFFER ) { 00301 DH_DbgPrint(MID_TRACE,("Error %d, New Buffer Size: %d\n", Error, Size)); 00302 free( Table ); 00303 Table = (PMIB_IFTABLE) malloc( Size ); 00304 } 00305 00306 if( Error != NO_ERROR ) 00307 { 00308 /* HACK: We are waiting until TCP/IP starts */ 00309 Sleep(2000); 00310 continue; 00311 } 00312 00313 DH_DbgPrint(MID_TRACE,("Got Adapter List (%d entries)\n", Table->dwNumEntries)); 00314 00315 for( i = Table->dwNumEntries - 1; i >= 0; i-- ) { 00316 DH_DbgPrint(MID_TRACE,("Getting adapter %d attributes\n", 00317 Table->table[i].dwIndex)); 00318 00319 ApiLock(); 00320 00321 if ((Adapter = AdapterFindByHardwareAddress(Table->table[i].bPhysAddr, Table->table[i].dwPhysAddrLen))) 00322 { 00323 proto = find_protocol_by_adapter(&Adapter->DhclientInfo); 00324 00325 /* This is an existing adapter */ 00326 if (InterfaceConnected(&Table->table[i])) { 00327 /* We're still active so we stay in the list */ 00328 ifi = &Adapter->DhclientInfo; 00329 00330 /* This is a hack because IP helper API sucks */ 00331 if (IsReconnectHackNeeded(Adapter, &Table->table[i])) 00332 { 00333 /* This handles a disconnect/reconnect */ 00334 00335 if (proto) 00336 remove_protocol(proto); 00337 Adapter->DhclientInfo.client->state = S_INIT; 00338 00339 /* These are already invalid since the media state change */ 00340 Adapter->RouterMib.dwForwardNextHop = 0; 00341 Adapter->NteContext = 0; 00342 00343 add_protocol(Adapter->DhclientInfo.name, 00344 Adapter->DhclientInfo.rfdesc, 00345 got_one, &Adapter->DhclientInfo); 00346 state_init(&Adapter->DhclientInfo); 00347 00348 SetEvent(AdapterStateChangedEvent); 00349 } 00350 00351 } else { 00352 if (proto) 00353 remove_protocol(proto); 00354 00355 /* We've lost our link so out we go */ 00356 RemoveEntryList(&Adapter->ListEntry); 00357 free(Adapter); 00358 } 00359 00360 ApiUnlock(); 00361 00362 continue; 00363 } 00364 00365 ApiUnlock(); 00366 00367 Adapter = (DHCP_ADAPTER*) calloc( sizeof( DHCP_ADAPTER ) + Table->table[i].dwMtu, 1 ); 00368 00369 if( Adapter && Table->table[i].dwType == MIB_IF_TYPE_ETHERNET && InterfaceConnected(&Table->table[i])) { 00370 memcpy( &Adapter->IfMib, &Table->table[i], 00371 sizeof(Adapter->IfMib) ); 00372 Adapter->DhclientInfo.client = &Adapter->DhclientState; 00373 Adapter->DhclientInfo.rbuf = Adapter->recv_buf; 00374 Adapter->DhclientInfo.rbuf_max = Table->table[i].dwMtu; 00375 Adapter->DhclientInfo.rbuf_len = 00376 Adapter->DhclientInfo.rbuf_offset = 0; 00377 memcpy(Adapter->DhclientInfo.hw_address.haddr, 00378 Adapter->IfMib.bPhysAddr, 00379 Adapter->IfMib.dwPhysAddrLen); 00380 Adapter->DhclientInfo.hw_address.hlen = Adapter->IfMib.dwPhysAddrLen; 00381 00382 /* I'm not sure where else to set this, but 00383 some DHCP servers won't take a zero. 00384 We checked the hardware type earlier in 00385 the if statement. */ 00386 Adapter->DhclientInfo.hw_address.htype = HTYPE_ETHER; 00387 00388 if( DhcpSocket == INVALID_SOCKET ) { 00389 DhcpSocket = 00390 Adapter->DhclientInfo.rfdesc = 00391 Adapter->DhclientInfo.wfdesc = 00392 socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); 00393 00394 if (DhcpSocket != INVALID_SOCKET) { 00395 00396 /* Allow broadcast on this socket */ 00397 Broadcast = 1; 00398 setsockopt(DhcpSocket, 00399 SOL_SOCKET, 00400 SO_BROADCAST, 00401 (const char *)&Broadcast, 00402 sizeof(Broadcast)); 00403 00404 Adapter->ListenAddr.sin_family = AF_INET; 00405 Adapter->ListenAddr.sin_port = htons(LOCAL_PORT); 00406 Adapter->BindStatus = 00407 (bind( Adapter->DhclientInfo.rfdesc, 00408 (struct sockaddr *)&Adapter->ListenAddr, 00409 sizeof(Adapter->ListenAddr) ) == 0) ? 00410 0 : WSAGetLastError(); 00411 } else { 00412 error("socket() failed: %d\n", WSAGetLastError()); 00413 } 00414 } else { 00415 Adapter->DhclientInfo.rfdesc = 00416 Adapter->DhclientInfo.wfdesc = DhcpSocket; 00417 } 00418 00419 Adapter->DhclientConfig.timeout = DHCP_PANIC_TIMEOUT; 00420 Adapter->DhclientConfig.initial_interval = DHCP_DISCOVER_INTERVAL; 00421 Adapter->DhclientConfig.retry_interval = DHCP_DISCOVER_INTERVAL; 00422 Adapter->DhclientConfig.select_interval = 1; 00423 Adapter->DhclientConfig.reboot_timeout = DHCP_REBOOT_TIMEOUT; 00424 Adapter->DhclientConfig.backoff_cutoff = DHCP_BACKOFF_MAX; 00425 Adapter->DhclientState.interval = 00426 Adapter->DhclientConfig.retry_interval; 00427 00428 if( PrepareAdapterForService( Adapter ) ) { 00429 Adapter->DhclientInfo.next = ifi; 00430 ifi = &Adapter->DhclientInfo; 00431 00432 read_client_conf(&Adapter->DhclientInfo); 00433 00434 if (Adapter->DhclientInfo.client->state == S_INIT) 00435 { 00436 add_protocol(Adapter->DhclientInfo.name, 00437 Adapter->DhclientInfo.rfdesc, 00438 got_one, &Adapter->DhclientInfo); 00439 00440 state_init(&Adapter->DhclientInfo); 00441 } 00442 00443 ApiLock(); 00444 InsertTailList( &AdapterList, &Adapter->ListEntry ); 00445 AdapterCount++; 00446 SetEvent(AdapterStateChangedEvent); 00447 ApiUnlock(); 00448 } else { free( Adapter ); Adapter = 0; } 00449 } else { free( Adapter ); Adapter = 0; } 00450 00451 if( !Adapter ) 00452 DH_DbgPrint(MID_TRACE,("Adapter %d was rejected\n", 00453 Table->table[i].dwIndex)); 00454 } 00455 #if 0 00456 Error = NotifyAddrChange(NULL, NULL); 00457 if (Error != NO_ERROR) 00458 break; 00459 #else 00460 Sleep(3000); 00461 #endif 00462 } while (TRUE); 00463 00464 DbgPrint("DHCPCSVC: Adapter discovery thread is terminating! (Error: %d)\n", Error); 00465 00466 if( Table ) free( Table ); 00467 return Error; 00468 } 00469 00470 HANDLE StartAdapterDiscovery(VOID) { 00471 HANDLE ThreadHandle, EventHandle; 00472 00473 EventHandle = CreateEvent(NULL, 00474 FALSE, 00475 FALSE, 00476 NULL); 00477 00478 ThreadHandle = CreateThread(NULL, 00479 0, 00480 AdapterDiscoveryThread, 00481 (LPVOID)EventHandle, 00482 0, 00483 NULL); 00484 00485 if (ThreadHandle == NULL) 00486 return NULL; 00487 00488 CloseHandle(ThreadHandle); 00489 00490 return EventHandle; 00491 } 00492 00493 void AdapterStop() { 00494 PLIST_ENTRY ListEntry; 00495 PDHCP_ADAPTER Adapter; 00496 ApiLock(); 00497 while( !IsListEmpty( &AdapterList ) ) { 00498 ListEntry = (PLIST_ENTRY)RemoveHeadList( &AdapterList ); 00499 Adapter = CONTAINING_RECORD( ListEntry, DHCP_ADAPTER, ListEntry ); 00500 free( Adapter ); 00501 } 00502 ApiUnlock(); 00503 WSACleanup(); 00504 } 00505 00506 PDHCP_ADAPTER AdapterFindIndex( unsigned int indx ) { 00507 PDHCP_ADAPTER Adapter; 00508 PLIST_ENTRY ListEntry; 00509 00510 for( ListEntry = AdapterList.Flink; 00511 ListEntry != &AdapterList; 00512 ListEntry = ListEntry->Flink ) { 00513 Adapter = CONTAINING_RECORD( ListEntry, DHCP_ADAPTER, ListEntry ); 00514 if( Adapter->IfMib.dwIndex == indx ) return Adapter; 00515 } 00516 00517 return NULL; 00518 } 00519 00520 PDHCP_ADAPTER AdapterFindName( const WCHAR *name ) { 00521 PDHCP_ADAPTER Adapter; 00522 PLIST_ENTRY ListEntry; 00523 00524 for( ListEntry = AdapterList.Flink; 00525 ListEntry != &AdapterList; 00526 ListEntry = ListEntry->Flink ) { 00527 Adapter = CONTAINING_RECORD( ListEntry, DHCP_ADAPTER, ListEntry ); 00528 if( !wcsicmp( Adapter->IfMib.wszName, name ) ) return Adapter; 00529 } 00530 00531 return NULL; 00532 } 00533 00534 PDHCP_ADAPTER AdapterFindInfo( struct interface_info *ip ) { 00535 PDHCP_ADAPTER Adapter; 00536 PLIST_ENTRY ListEntry; 00537 00538 for( ListEntry = AdapterList.Flink; 00539 ListEntry != &AdapterList; 00540 ListEntry = ListEntry->Flink ) { 00541 Adapter = CONTAINING_RECORD( ListEntry, DHCP_ADAPTER, ListEntry ); 00542 if( ip == &Adapter->DhclientInfo ) return Adapter; 00543 } 00544 00545 return NULL; 00546 } 00547 00548 PDHCP_ADAPTER AdapterFindByHardwareAddress( u_int8_t haddr[16], u_int8_t hlen ) { 00549 PDHCP_ADAPTER Adapter; 00550 PLIST_ENTRY ListEntry; 00551 00552 for(ListEntry = AdapterList.Flink; 00553 ListEntry != &AdapterList; 00554 ListEntry = ListEntry->Flink) { 00555 Adapter = CONTAINING_RECORD( ListEntry, DHCP_ADAPTER, ListEntry ); 00556 if (Adapter->DhclientInfo.hw_address.hlen == hlen && 00557 !memcmp(Adapter->DhclientInfo.hw_address.haddr, 00558 haddr, 00559 hlen)) return Adapter; 00560 } 00561 00562 return NULL; 00563 } 00564 00565 PDHCP_ADAPTER AdapterGetFirst() { 00566 if( IsListEmpty( &AdapterList ) ) return NULL; else { 00567 return CONTAINING_RECORD 00568 ( AdapterList.Flink, DHCP_ADAPTER, ListEntry ); 00569 } 00570 } 00571 00572 PDHCP_ADAPTER AdapterGetNext( PDHCP_ADAPTER This ) 00573 { 00574 if( This->ListEntry.Flink == &AdapterList ) return NULL; 00575 return CONTAINING_RECORD 00576 ( This->ListEntry.Flink, DHCP_ADAPTER, ListEntry ); 00577 } 00578 00579 void if_register_send(struct interface_info *ip) { 00580 00581 } 00582 00583 void if_register_receive(struct interface_info *ip) { 00584 } Generated on Sun May 27 2012 04:21:13 for ReactOS by
1.7.6.1
|