{-# LANGUAGE OverloadedStrings #-}
-- |
-- module: System.Win32.DHCP
-- copyright: (c) Michael Steele, 2014
-- license: BSD3
-- maintainer: mikesteele81@gmail.com
-- stability: experimental
-- portability: Windows
module System.Win32.DHCP
( DhcpApi ()
, loadDHCP
-- * General Types
, Context (..)
, ClientType (..)
, DATE_TIME (..)
, HOST_INFO (..)
, SEARCH_INFO (..)
-- * Leases
, Client (..)
, enumClients
, lookupClient
, deleteClient
-- * Reservations
, Mapping (..)
, Reservation(..)
, addReservation
, enumReservations
, removeReservation
) where
import Control.Monad (unless)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Import
import Data.Ip
import System.Win32.DHCP.DhcpStructure
import System.Win32.DHCP.HOST_INFO
import System.Win32.DHCP.Internal
import System.Win32.DHCP.Client
import System.Win32.DHCP.LengthBuffer
import System.Win32.DHCP.Reservation
import System.Win32.DHCP.SEARCH_INFO
import System.Win32.DHCP.SUBNET_CLIENT_INFO_ARRAY_V4
import System.Win32.DHCP.SUBNET_ELEMENT_DATA_V4
import System.Win32.DHCP.SUBNET_ELEMENT_INFO_ARRAY_V4
import System.Win32.DHCP.Types
-- | A Context defines which server and scope within that server a command
-- refers to. Microsoft's DHCP server supports multiple scopes. This allows
-- different configurations to be sent to devices based on their hardware
-- (MAC) address. Scopes are identified by their network address.
data Context = Context
{ -- | The DHCP server management API uses an RPC mechanism to control the
-- server.
contextServer :: !Text
, -- | Scopes are identified by a subnet identifier. This is useful in
-- cases where multiple scopes are defined, but still required in the
-- common case of a single scope.
contextSubnet :: !Ip
} deriving (Eq, Ord)
-- | Delete an active DHCP lease from the server.
-- The `SEARCH_INFO` argument determines which search criteria
-- to use. Searching by name will delete all active leases
-- with that name. This action corresponds to MSDN's DhcpDeleteClientInfoV4
-- function.
deleteClient :: DhcpApi
-> Text
-- ^ String which specifies the IP address or hostname of the DHCP
-- server.
-> SEARCH_INFO
-- ^ Define how to lookup a client to delete. Only deleting based on
-- IP addresses have been tested.
-> IO ()
-- ^ This function will throw an 'Win32Exception' when the internal Win32
-- call returnes an error condition. MSDN lists the following exceptions,
-- but others might be thrown as well:
--
-- ['DhcpReservedClient'] The specified DHCP client is a reserved DHCP
-- client.
--
-- ['DhcpJetError'] An error occurred while accessing the DHCP server
-- database, or the client entry is not present in the database.
deleteClient api server si =
withTString server $ \pserver ->
withSearchInfo si $ \psi ->
failUnlessSuccess "DeleteClientInfo"
$ c_DeleteClientInfo api pserver psi
-- | Perform a lookup operation for all client lease records within a
-- scope. This action corresponds to MSDN's DhcpEnumSubnetClientsV4 function.
enumClients :: DhcpApi
-> Context
-- ^ Specify which server and scope to search for client leases.
-> IO [Client]
-- ^ The empty list means that no client records exist for the provided
-- subnet. This function will throw an 'Win32Exception' when the
-- internal Win32 call returnes an error condition. MSDN lists the
-- following exceptions, but others might be thrown as well:
--
-- ['DhcpJetError'] An error occurred while accessing the DHCP server
-- database.
enumClients dhcp (Context server subnet) =
enumSubnetClientsV4 dhcp server subnet
-- | Search the DHCP server for a lease matching the given search criteria.
-- `Nothing` is returned when no lease was found. This corresponds to MSDN's
-- DhcpGetClientInfoV4 function.
lookupClient :: DhcpApi
-> Text
-- ^ According to MSDN this must specify the IP address of the server.
-- Other functions (including this one) may or may not also accept
-- a Unicode host name.
-> SEARCH_INFO
-- ^ Define how to lookup a client. Only searching based on an
-- IP addresses has been tested.
-> IO (Maybe Client)
-- ^ A 'Nothing' indicates that no client was found. This function will
-- throw an 'Win32Exception' when the internal Win32 call returnes an
-- error condition. MSDN lists the following exceptions, but others might
-- be thrown as well:
--
-- ['DhcpJetError'] An error occurred while accessing the DHCP server
-- database.
lookupClient api serverip si =
withTString serverip $ \pserverip ->
withSearchInfo si $ \psi ->
with nullPtr $ \ppclientinfo -> do
-- DhcpGetClientInfo takes a structure on the stack.
siType <- peek (castPtr psi :: Ptr CInt)
siPayload <- peekElemOff (castPtr psi :: Ptr (Ptr ())) 4
failUnlessSuccess "GetClientInfoV4"
$ c_GetClientInfoV4 api pserverip siType siPayload ppclientinfo
-- Extract client information from out parameter and free the memory
-- 'Nothing' will be returned the case where GetClientInfoV4 returns a
-- null pointer.
scrubWith ppclientinfo $ \pclientinfo -> do
clientinfo <- peekDhcp clientInfo pclientinfo
freeDhcp clientInfo (rpcFreeMemory api) pclientinfo
return clientinfo
addReservation :: DhcpApi -> Context -> Mapping
-> IO ()
-- ^ This function will throw an 'Win32Exception' when the internal Win32
-- call returnes an error condition. MSDN lists the following exceptions,
-- but others might be thrown as well:
--
-- ['DhcpSubnetNotPresent'] The specified IPv4 subnet does not exist
-- on the DHCP server.
--
-- ['DhcpJetError'] An error occurred while accessing the DHCP server
-- database.
--
-- ['DhcpNotReservedClient'] The specified DHCP client is not a
-- reserved client.
--
-- ['DhcpInvalidRange'] The specified IPv4 range does not match an
-- existing IPv4 range.
--
-- ['ScopeRangePolicyChangeConflict'] An IP address range is
-- configured for a policy in this scope. This operation cannot be
-- performed on the scope IP address range until the policy IP address
-- range is suitably modified.
addReservation dhcp (Context server subnet) mapping =
addSubnetElementV4 dhcp server subnet
$ ReservedIps (Reservation mapping Both)
enumReservations :: DhcpApi -> Context -> IO [Reservation]
enumReservations dhcp (Context server subnet) = do
ex <- enumSubnetElementsV4 dhcp server subnet 2
res <- flip mapM ex $ \element -> do
case element of
ReservedIps res -> return res
_ -> error "bug in Win32 API."
return res
-- | Remove a reservation from the server
removeReservation :: DhcpApi -> Context -> Mapping
-> ClientType
-- ^ Specify a DHCP reservation, BOOTP reservation, or both. This is
-- untested.
-> Bool
-- ^ Specify whether any active leases for the reservation should be
-- removed as well.
-> IO ()
-- ^ This function will throw an 'Win32Exception' when the internal Win32
-- call returnes an error condition. MSDN lists the following exceptions,
-- but others might be thrown as well:
--
-- ['DhcpSubnetNotPresent'] The specified IPv4 subnet does not exist
-- on the DHCP server.
--
-- ['DhcpElementCantRemove'] Failure can occur for any number of
-- reasons.
--
-- ['DhcpJetError'] An error occurred while accessing the DHCP server
-- database.
--
-- ['DhcpInvalidRange'] The specified IPv4 range does not match an
-- existing IPv4 range.
--
-- ['ScopeRangePolicyChangeConflict'] An IP address range is
-- configured for a policy in this scope. This operation cannot be
-- performed on the scope IP address range until the policy IP address
-- range is suitably modified.
removeReservation dhcp (Context server subnet) mapping ct force =
removeSubnetElementV4 dhcp server subnet
(ReservedIps $ Reservation mapping ct) fForce
where
fForce = if force then FullForce else NoForce
enumSubnetClientsV4 :: DhcpApi -> Text -> Ip -> IO [Client]
enumSubnetClientsV4 dhcp server subnet =
withTString server $ \pServer ->
-- inout parameter. Supply on successive calls to retreive more elements
with 0 $ \pResumeHandle ->
alloca $ \pElementsRead ->
alloca $ \pElementsTotal ->
with nullPtr $ \ppInfoArray -> do
-- We have to call enumSubnetElementsV4 at least twice for a sucessfull
-- run. Failure to use the returned resumeHandle may result in an
-- internal access violation within RPCRT4.dll.
let loop acc = do
ret <- fromDWORD <$> c_EnumSubnetClientsV4 dhcp pServer (toWord32 subnet)
pResumeHandle 0xFFFFFFFF ppInfoArray
pElementsRead pElementsTotal
unless (elem ret [Success, MoreData, NoMoreItems])
$ failWith "EnumSubnetClientsV4" ret
melems <- scrubWith ppInfoArray $ \pInfoArray -> do
SUBNET_CLIENT_INFO_ARRAY_V4 (LengthBuffer _ elems) <- peekDhcp clientInfoArray pInfoArray
freeDhcp clientInfoArray (rpcFreeMemory dhcp) pInfoArray
return elems
let elems = fromMaybe [] melems
if (ret == NoMoreItems || ret == Success)
then return (elems:acc)
else loop (elems:acc)
revelemss <- loop []
return $ concat $ reverse revelemss
enumSubnetElementsV4 :: DhcpApi -> Text -> Ip -> CInt
-> IO [SUBNET_ELEMENT_DATA_V4]
enumSubnetElementsV4 dhcp server subnet elementType =
withTString server $ \pServer ->
-- inout parameter. Supply on successive calls to retreive more elements
with 0 $ \pResumeHandle ->
alloca $ \pElementsRead ->
alloca $ \pElementsTotal ->
with nullPtr $ \ppInfoArray -> do
-- We have to call enumSubnetElementsV4 at least twice for a sucessfull
-- run. Failure to use the returned resumeHandle may result in an
-- internal access violation within RPCRT4.dll.
let loop acc = do
ret <- fromDWORD <$> c_EnumSubnetElementsV4 dhcp pServer (toWord32 subnet) elementType
pResumeHandle 0xFFFFFFFF ppInfoArray
pElementsRead pElementsTotal
unless (elem ret [Success, MoreData, NoMoreItems])
$ failWith "EnumSubnetElementsV4" ret
melems <- scrubWith ppInfoArray $ \pInfoArray -> do
SUBNET_ELEMENT_INFO_ARRAY_V4 (LengthBuffer _ elems) <- peekDhcp infoArray pInfoArray
freeDhcp infoArray (rpcFreeMemory dhcp) pInfoArray
return elems
let elems = fromMaybe [] melems
-- Intentionally ignore elementsTotal. Microsoft's documentation
-- on how this should work doesn't seem right. In my testing
-- elementsTotal always returns 0x7fffffff until the last loop, at
-- which time it always matches elementsRead.
if (ret == NoMoreItems || ret == Success)
then return (elems:acc)
else loop (elems:acc)
revelemss <- loop []
return $ concat $ reverse revelemss
addSubnetElementV4 :: DhcpApi -> Text -> Ip -> SUBNET_ELEMENT_DATA_V4 -> IO ()
addSubnetElementV4 dhcp server subnet elementData =
withTString server $ \pServer ->
withDhcp subnetElementData elementData $ \pElementData ->
failUnlessSuccess "AddSubnetElementsV4"
$ c_AddSubnetElementV4 dhcp pServer (toWord32 subnet) pElementData
-- | Remove an IPv4 subnet element from an IPv4 subnet defined on the DHCPv4
-- server.
removeSubnetElementV4
:: DhcpApi
-> Text -- ^ Unicode string that specifies the IP address, such as
-- \"123.456.789.012\", or hostname, such as \"server\",
-- of the DHCP server.
-> Ip -- ^ 'DWORD' value that specifies the IP address of the
-- subnet gateway and uniquely identifies it. As an
-- example, with the above IP address and a subnet mask
-- of 255.255.255.0, the subnet would be 123.456.789.0.
-- This must then be converted to a 'DWORD' value of
-- 2,093,683,968.
-> SUBNET_ELEMENT_DATA_V4 -- ^ 'DHCP_SUBNET_ELEMENT_DATA_V4' structure
-- that contains information used to find the
-- element that will be removed from subnet
-- specified in SubnetAddress.
-> FORCE_FLAG -- ^ DHCP_FORCE_FLAG enumeration value that indicates
-- whether or not the clients affected by the removal of
-- the subnet element should also be deleted.
--
-- Note If the flag is set to DhcpNoForce and this subnet
-- has served an IPv4 address to DHCPv4/BOOTP clients, the
-- IPv4 range is not deleted; conversely, if the flag is
-- set to DhcpFullForce, the IPv4 range is deleted along
-- with the DHCPv4 client lease record on the DHCPv4
-- server.
-> IO ()
-- ^ This function will throw an 'Win32Exception' when the internal Win32
-- call returnes an error condition. MSDN lists the following exceptions,
-- but others might be thrown as well:
--
-- ['DhcpSubnetNotPresent'] The specified IPv4 subnet does not exist
-- on the DHCP server.
--
-- ['DhcpElementCantRemove'] Failure can occur for any number of
-- reasons.
--
-- ['DhcpJetError'] An error occurred while accessing the DHCP server
-- database.
--
-- ['DhcpInvalidRange'] The specified IPv4 range does not match an
-- existing IPv4 range.
--
-- ['ScopeRangePolicyChangeConflict'] An IP address range is
-- configured for a policy in this scope. This operation cannot be
-- performed on the scope IP address range until the policy IP address
-- range is suitably modified.
removeSubnetElementV4 dhcp server subnet elementData forceFlag =
withTString server $ \pServer ->
withDhcp subnetElementData elementData $ \pElementData ->
failUnlessSuccess "RemoveSubnetElementV4"
$ c_RemoveSubnetElementV4 dhcp pServer (toWord32 subnet) pElementData
(fromIntegral . fromEnum $ forceFlag)