Refactoring: split forms, validation errors, generic data types.
This commit is contained in:
		
							parent
							
								
									a3bdecb1fd
								
							
						
					
					
						commit
						4b59d52684
					
				
					 8 changed files with 575 additions and 522 deletions
				
			
		|  | @ -1,45 +1,7 @@ | |||
| module App.Type.Delegation where | ||||
| module App.Type.Delegation | ||||
|   ( module App.Type.Form.Delegation | ||||
|   , module App.Type.Error.Delegation | ||||
|   ) where | ||||
| 
 | ||||
| import GenericParser.Parser as G | ||||
| import GenericParser.DomainParser.Common (DomainError) as DomainParser | ||||
| 
 | ||||
| -- | The required data needed to properly delegate a domain: two name servers. | ||||
| -- | The type also includes potential errors found while validating the data. | ||||
| type Form | ||||
|   = { nameserver1 :: String | ||||
|     , nameserver2 :: String | ||||
|     , errors      :: Array Error | ||||
|     } | ||||
| 
 | ||||
| -- | Empty delegation form, with default inputs. | ||||
| mkEmptyDelegationForm :: Form | ||||
| mkEmptyDelegationForm | ||||
|   = { nameserver1: "ns0.example.com" | ||||
|     , nameserver2: "ns1.example.com" | ||||
|     , errors: [] | ||||
|     } | ||||
| 
 | ||||
| -- | What are the **fields** of our delegation form? | ||||
| -- | This *Field* data type provides a way to update the form with `update`. | ||||
| data Field | ||||
|   = NameServer1 String | ||||
|   | NameServer2 String | ||||
| 
 | ||||
| -- | Utility function to update a field of the form, based on the previous `Form` and `Field` types. | ||||
| -- | | ||||
| -- | RATIONALE: this utility function enables a generic way of handling field updates. | ||||
| -- | In Halogen, a single *Action* is required to update all fields: | ||||
| -- |``` | ||||
| -- |  UpdateDelegationForm field -> do | ||||
| -- |    state <- H.get | ||||
| -- |    H.modify_ _ { delegation_form = Delegation.update state.delegation_form field } | ||||
| -- |``` | ||||
| update :: Form -> Field -> Form | ||||
| update form updated_field = case updated_field of | ||||
|   NameServer1 val -> form { nameserver1 = val } | ||||
|   NameServer2 val -> form { nameserver2 = val } | ||||
| 
 | ||||
| -- | Possible errors regarding the form (domain parsing errors). | ||||
| data Error | ||||
|   = VENameServer1 (G.Error DomainParser.DomainError) | ||||
|   | VENameServer2 (G.Error DomainParser.DomainError) | ||||
| import App.Type.Form.Delegation (Form, Field(..), update, mkEmptyDelegationForm) | ||||
| import App.Type.Error.Delegation (Error(..)) | ||||
|  |  | |||
							
								
								
									
										10
									
								
								src/App/Type/Error/Delegation.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/App/Type/Error/Delegation.purs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| -- | Possible errors while verifying the Delegation form. | ||||
| module App.Type.Error.Delegation where | ||||
| 
 | ||||
| import GenericParser.Parser as G | ||||
| import GenericParser.DomainParser.Common (DomainError) as DomainParser | ||||
| 
 | ||||
| -- | Possible errors regarding the form (domain parsing errors). | ||||
| data Error | ||||
|   = VENameServer1 (G.Error DomainParser.DomainError) | ||||
|   | VENameServer2 (G.Error DomainParser.DomainError) | ||||
							
								
								
									
										56
									
								
								src/App/Type/Error/ResourceRecord.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/App/Type/Error/ResourceRecord.purs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | |||
| module App.Type.Error.ResourceRecord where | ||||
| 
 | ||||
| import Prelude (class Show, ($), (-), (<>)) | ||||
| 
 | ||||
| import GenericParser.Parser as G | ||||
| import GenericParser.IPAddress as IPAddress | ||||
| import GenericParser.DomainParser.Common (DomainError) as DomainParser | ||||
| 
 | ||||
| -- | Errors that might be catched in for the form upon validation (`App.Validation.DNS`). | ||||
| -- | | ||||
| -- | **History:** | ||||
| -- | The module once used dedicated types for each type of RR. | ||||
| -- | That comes with several advantages. | ||||
| -- | First, type verification was a thing, and function were dedicated to a certain type of record. | ||||
| -- | Second, these dedicated types used strings for their fields, | ||||
| -- | which simplifies the typing when dealing with forms. | ||||
| -- | Finally, the validation was a way to convert dedicated types (used in forms) | ||||
| -- | to the general type (used for network serialization). | ||||
| -- | This ensures each resource record is verified before being sent to `dnsmanagerd`. | ||||
| -- | | ||||
| -- | The problem is that, with dedicated types, you are then required to have dedicated functions. | ||||
| -- | Conversion functions are also required. | ||||
| -- | | ||||
| -- | Maybe the code will change again in the future, but for now it will be enough. | ||||
| 
 | ||||
| data Error | ||||
|   = UNKNOWN | ||||
|   | VEIPv4 (G.Error IPAddress.IPv4Error) | ||||
|   | VEIPv6 (G.Error IPAddress.IPv6Error) | ||||
|   | VEName (G.Error DomainParser.DomainError) | ||||
|   | VETTL Int Int Int | ||||
|   | VETXT (G.Error TXTError) | ||||
|   | VECNAME (G.Error DomainParser.DomainError) | ||||
|   | VENS (G.Error DomainParser.DomainError) | ||||
|   | VEMX (G.Error DomainParser.DomainError) | ||||
|   | VEPriority Int Int Int | ||||
|   | VESRV (G.Error DomainParser.DomainError) | ||||
|   | VEPort Int Int Int | ||||
|   | VEWeight Int Int Int | ||||
|   | VEDMARCpct Int Int Int | ||||
|   | VEDMARCri  Int Int Int | ||||
| 
 | ||||
|   | VECAAflag  Int Int Int -- CAA flag should be between 0 and 255 (1 byte). | ||||
| 
 | ||||
|   -- SPF | ||||
|   | VESPFMechanismName (G.Error DomainParser.DomainError) | ||||
|   | VESPFMechanismIPv4 (G.Error IPAddress.IPv4Error) | ||||
|   | VESPFMechanismIPv6 (G.Error IPAddress.IPv6Error) | ||||
| 
 | ||||
|   | VESPFModifierName (G.Error DomainParser.DomainError) | ||||
| 
 | ||||
|   | DKIMInvalidKeySize Int Int | ||||
| 
 | ||||
| data TXTError | ||||
|   = TXTInvalidCharacter | ||||
|   | TXTTooLong Int Int -- max current | ||||
							
								
								
									
										39
									
								
								src/App/Type/Form/Delegation.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/App/Type/Form/Delegation.purs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| module App.Type.Form.Delegation where | ||||
| 
 | ||||
| import App.Type.Error.Delegation (Error) | ||||
| 
 | ||||
| -- | The required data needed to properly delegate a domain: two name servers. | ||||
| -- | The type also includes potential errors found while validating the data. | ||||
| type Form | ||||
|   = { nameserver1 :: String | ||||
|     , nameserver2 :: String | ||||
|     , errors      :: Array Error | ||||
|     } | ||||
| 
 | ||||
| -- | Empty delegation form, with default inputs. | ||||
| mkEmptyDelegationForm :: Form | ||||
| mkEmptyDelegationForm | ||||
|   = { nameserver1: "ns0.example.com" | ||||
|     , nameserver2: "ns1.example.com" | ||||
|     , errors: [] | ||||
|     } | ||||
| 
 | ||||
| -- | What are the **fields** of our delegation form? | ||||
| -- | This *Field* data type provides a way to update the form with `update`. | ||||
| data Field | ||||
|   = NameServer1 String | ||||
|   | NameServer2 String | ||||
| 
 | ||||
| -- | Utility function to update a field of the form, based on the previous `Form` and `Field` types. | ||||
| -- | | ||||
| -- | RATIONALE: this utility function enables a generic way of handling field updates. | ||||
| -- | In Halogen, a single *Action* is required to update all fields: | ||||
| -- |``` | ||||
| -- |  UpdateDelegationForm field -> do | ||||
| -- |    state <- H.get | ||||
| -- |    H.modify_ _ { delegation_form = Delegation.update state.delegation_form field } | ||||
| -- |``` | ||||
| update :: Form -> Field -> Form | ||||
| update form updated_field = case updated_field of | ||||
|   NameServer1 val -> form { nameserver1 = val } | ||||
|   NameServer2 val -> form { nameserver2 = val } | ||||
							
								
								
									
										240
									
								
								src/App/Type/Form/ResourceRecord.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/App/Type/Form/ResourceRecord.purs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,240 @@ | |||
| module App.Type.Form.ResourceRecord where | ||||
| 
 | ||||
| import Prelude (($), (-), (<>)) | ||||
| 
 | ||||
| import Data.Maybe (Maybe(..), fromMaybe, maybe) | ||||
| import Data.Array as A | ||||
| import Data.Int (fromString) | ||||
| import Data.Either (Either(..)) | ||||
| 
 | ||||
| import Utils (id, attach_id, remove_id) | ||||
| 
 | ||||
| import App.Validation.Email as Email | ||||
| 
 | ||||
| import App.Type.ResourceRecord.AcceptedRRTypes (AcceptedRRTypes(..)) | ||||
| 
 | ||||
| import App.Type.ResourceRecord.ResourceRecord (ResourceRecord, default_caa, default_rr, srv_protocols) | ||||
| import App.Type.ResourceRecord.CAA as CAA | ||||
| import App.Type.ResourceRecord.DKIM as DKIM | ||||
| import App.Type.ResourceRecord.DMARC as DMARC | ||||
| import App.Type.ResourceRecord.SPF as SPF | ||||
| 
 | ||||
| import App.Type.Error.ResourceRecord (Error) | ||||
| 
 | ||||
| -- | `Form` is the necessary state to modify a resource record. | ||||
| -- | It contains the currently manipulated record, detected errors, along with some temporary values. | ||||
| -- | FIXME: this form is messy AF and should be replaced. | ||||
| type Form = | ||||
|   { _rr                :: ResourceRecord | ||||
|   , _errors            :: Array Error | ||||
|   , _dmarc_mail_errors :: Array Email.Error | ||||
|   , _zonefile          :: Maybe String | ||||
|   , tmp                :: TMP | ||||
|   } | ||||
| 
 | ||||
| data Field | ||||
|   = Domain    String | ||||
|   | TTL       String | ||||
|   | Target    String | ||||
|   | Priority  String | ||||
|   | Weight    String | ||||
|   | Port      String | ||||
|   | SPF_v          String | ||||
|   | SPF_mechanisms (Array SPF.Mechanism) | ||||
|   | SPF_modifiers  (Array SPF.Modifier) | ||||
|   | SPF_q          SPF.Qualifier | ||||
| 
 | ||||
|   | CAA_flag  String | ||||
|   | CAA_value String | ||||
| 
 | ||||
| -- | TMP: temporary stored values regarding specific records such as SPF, | ||||
| -- | DKIM and DMARC. | ||||
| type TMP = | ||||
|   { | ||||
|     -- SPF details. | ||||
|     spf :: { mechanism_q :: String | ||||
|            , mechanism_t :: String | ||||
|            , mechanism_v :: String | ||||
|            , modifier_t  :: String | ||||
|            , modifier_v  :: String | ||||
|            } | ||||
| 
 | ||||
|     -- DMARC details. | ||||
|   , dmarc_mail       :: String | ||||
|   , dmarc_mail_limit :: Maybe Int | ||||
|   , dmarc            :: DMARC.DMARC | ||||
| 
 | ||||
|     -- DKIM details. | ||||
|   , dkim :: DKIM.DKIM | ||||
|   } | ||||
| 
 | ||||
| mkEmptyRRForm :: Form | ||||
| mkEmptyRRForm = | ||||
|   { | ||||
|   -- This is the state for the new RR modal. | ||||
|     _rr: default_rr A "" | ||||
|   -- List of errors within the form in new RR modal. | ||||
|   , _errors: [] | ||||
|   , _dmarc_mail_errors: [] | ||||
|   , _zonefile: Nothing | ||||
|   , tmp: { spf: { mechanism_q: "pass" | ||||
|                 , mechanism_t: "a" | ||||
|                 , mechanism_v: "" | ||||
|                 , modifier_t:  "redirect" | ||||
|                 , modifier_v:  "" | ||||
|                 } | ||||
|          , dkim:  DKIM.emptyDKIMRR | ||||
|          , dmarc: DMARC.emptyDMARCRR | ||||
|          , dmarc_mail: "" | ||||
|          , dmarc_mail_limit: Nothing | ||||
|          } | ||||
|   } | ||||
| 
 | ||||
| data RRUpdateValue | ||||
|   = CAA_tag Int | ||||
|   | SRV_Protocol Int | ||||
|   | SPF_Mechanism_q Int | ||||
|   | SPF_Mechanism_t Int | ||||
|   | SPF_Mechanism_v String | ||||
|   | SPF_Modifier_t  Int | ||||
|   | SPF_Modifier_v  String | ||||
|   | SPF_Qualifier   Int | ||||
| 
 | ||||
|   -- | Remove a SPF mechanism of the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_remove_mechanism Int | ||||
|   -- | Remove a SPF modifier of the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_remove_modifier Int | ||||
| 
 | ||||
|   -- | Add a SPF mechanism to the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_Mechanism_Add | ||||
|   -- | Add a SPF modifier to the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_Modifier_Add | ||||
| 
 | ||||
|   -- | Change the temporary mail address for DMARC. | ||||
|   | DMARC_mail String | ||||
| 
 | ||||
|   -- | Change the temporary report size limit for DMARC. | ||||
|   | DMARC_mail_limit String | ||||
| 
 | ||||
|   -- | Change the requested report interval. | ||||
|   | DMARC_ri String | ||||
| 
 | ||||
|   -- | Add a new mail address to the DMARC rua list. | ||||
|   | DMARC_rua_Add | ||||
| 
 | ||||
|   -- | Add a new mail address to the DMARC ruf list. | ||||
|   | DMARC_ruf_Add | ||||
| 
 | ||||
|   -- | Remove a mail address of the DMARC rua list. | ||||
|   | DMARC_remove_rua Int | ||||
| 
 | ||||
|   -- | Remove a mail address of the DMARC ruf list. | ||||
|   | DMARC_remove_ruf Int | ||||
| 
 | ||||
|   | DMARC_policy Int | ||||
|   | DMARC_sp_policy Int | ||||
|   | DMARC_adkim Int | ||||
|   | DMARC_aspf Int | ||||
|   | DMARC_pct  String | ||||
|   | DMARC_fo Int | ||||
| 
 | ||||
|   | DKIM_hash_algo Int | ||||
|   | DKIM_sign_algo Int | ||||
|   | DKIM_pubkey    String | ||||
|   | DKIM_note      String | ||||
| 
 | ||||
| update_form :: Form -> RRUpdateValue -> Form | ||||
| update_form form new_field_value = | ||||
|   case new_field_value of | ||||
|   CAA_tag         v -> | ||||
|     let new_tag = fromMaybe CAA.Issue $ CAA.tags A.!! v | ||||
|         new_value = case new_tag of | ||||
|           CAA.Issue -> "letsencrypt.org" | ||||
|           CAA.ContactEmail -> "contact@example.com" | ||||
|           CAA.ContactPhone -> "0203040506" | ||||
|           _                -> "" | ||||
|         new_caa = (fromMaybe default_caa form._rr.caa) { tag = new_tag, value = new_value } | ||||
|     in form { _rr { caa = Just new_caa } } | ||||
| 
 | ||||
|   SRV_Protocol v    -> form { _rr { protocol = srv_protocols A.!! v } } | ||||
|   SPF_Mechanism_q v -> form { tmp { spf { mechanism_q = maybe "pass"     id $ SPF.qualifier_types A.!! v  }}} | ||||
|   SPF_Mechanism_t v -> form { tmp { spf { mechanism_t = maybe "a"        id $ SPF.mechanism_types A.!! v  }}} | ||||
|   SPF_Mechanism_v v -> form { tmp { spf { mechanism_v = v                                                 }}} | ||||
|   SPF_Modifier_t v  -> form { tmp { spf { modifier_t  = maybe "redirect" id $ SPF.modifier_types  A.!! v  }}} | ||||
|   SPF_Modifier_v v  -> form { tmp { spf { modifier_v  = v                                                 }}} | ||||
|   SPF_Qualifier v   -> form { _rr { q  = SPF.qualifiers A.!! v                                             }} | ||||
|   SPF_remove_mechanism i -> | ||||
|     form { _rr { mechanisms = case form._rr.mechanisms of | ||||
|                    Just ms -> Just (remove_id i $ attach_id 0 ms) | ||||
|                    Nothing -> Nothing | ||||
|                } } | ||||
|   SPF_remove_modifier i -> | ||||
|     form { _rr { modifiers = case form._rr.modifiers of | ||||
|                    Just ms -> Just (remove_id i $ attach_id 0 ms) | ||||
|                    Nothing -> Nothing | ||||
|                } } | ||||
| 
 | ||||
|   SPF_Mechanism_Add -> | ||||
|     let m = form._rr.mechanisms | ||||
|         m_q = form.tmp.spf.mechanism_q | ||||
|         m_t = form.tmp.spf.mechanism_t | ||||
|         m_v = form.tmp.spf.mechanism_v | ||||
|         new_list_of_mechanisms = maybe [] id m <> maybe [] (\x -> [x]) (SPF.to_mechanism m_q m_t m_v) | ||||
|         new_value = case new_list_of_mechanisms of | ||||
|           [] -> Nothing | ||||
|           v  -> Just v | ||||
|     in form { _rr { mechanisms = new_value }} | ||||
| 
 | ||||
|   SPF_Modifier_Add -> | ||||
|     let m = form._rr.modifiers | ||||
|         m_t = form.tmp.spf.modifier_t | ||||
|         m_v = form.tmp.spf.modifier_v | ||||
|         new_list_of_modifiers = maybe [] id m <> maybe [] (\x -> [x]) (SPF.to_modifier m_t m_v) | ||||
|         new_value = case new_list_of_modifiers of | ||||
|           [] -> Nothing | ||||
|           v  -> Just v | ||||
|     in form { _rr { modifiers = new_value }} | ||||
| 
 | ||||
|   DMARC_mail       v -> form { tmp { dmarc_mail = v } } | ||||
|   DMARC_mail_limit v -> form { tmp { dmarc_mail_limit = Just $ fromMaybe 0 $ fromString v } } | ||||
|   DMARC_ri         v -> form { tmp { dmarc { ri = fromString v } } } | ||||
|   DMARC_rua_Add -> | ||||
|     case Email.email form.tmp.dmarc_mail of | ||||
|       Left errors -> form { _dmarc_mail_errors = errors } | ||||
|       Right _     -> | ||||
|         let current_ruas = fromMaybe [] form.tmp.dmarc.rua | ||||
|             new_list = current_ruas <> [ {mail: form.tmp.dmarc_mail, limit: form.tmp.dmarc_mail_limit} ] | ||||
|         in form { tmp { dmarc { rua = Just new_list }}} | ||||
| 
 | ||||
|   DMARC_ruf_Add -> | ||||
|     case Email.email form.tmp.dmarc_mail of | ||||
|       Left errors -> form { _dmarc_mail_errors = errors } | ||||
|       Right _     -> | ||||
|         let current_rufs = fromMaybe [] form.tmp.dmarc.ruf | ||||
|             new_list = current_rufs <> [ {mail: form.tmp.dmarc_mail, limit: form.tmp.dmarc_mail_limit} ] | ||||
|         in form { tmp { dmarc { ruf = Just new_list }}} | ||||
| 
 | ||||
|   DMARC_remove_rua i -> | ||||
|     let current_ruas = fromMaybe [] form.tmp.dmarc.rua | ||||
|         new_value = case (remove_id i $ attach_id 0 current_ruas) of | ||||
|           [] -> Nothing | ||||
|           v -> Just v | ||||
|     in form { tmp { dmarc { rua = new_value } } } | ||||
| 
 | ||||
|   DMARC_remove_ruf i -> | ||||
|     let current_rufs = fromMaybe [] form.tmp.dmarc.ruf | ||||
|         new_value = case (remove_id i $ attach_id 0 current_rufs) of | ||||
|           [] -> Nothing | ||||
|           v -> Just v | ||||
|     in form { tmp { dmarc { ruf = new_value } } } | ||||
| 
 | ||||
|   DMARC_policy    v -> form { tmp { dmarc { p     = fromMaybe DMARC.None $ DMARC.policies A.!! v       } } } | ||||
|   DMARC_sp_policy v -> form { tmp { dmarc { sp    =                        DMARC.policies A.!! (v - 1) } } } | ||||
|   DMARC_adkim     v -> form { tmp { dmarc { adkim = DMARC.consistency_policies A.!! (v - 1)            } } } | ||||
|   DMARC_aspf      v -> form { tmp { dmarc { aspf  = DMARC.consistency_policies A.!! (v - 1)            } } } | ||||
|   DMARC_pct       v -> form { tmp { dmarc { pct   = Just $ fromMaybe 100 (fromString v)                } } } | ||||
|   DMARC_fo        v -> form { tmp { dmarc { fo    = DMARC.report_occasions A.!! (v - 1)                } } } | ||||
|   DKIM_hash_algo  v -> form { tmp { dkim  { h     = DKIM.hash_algos A.!! v                             } } } | ||||
|   DKIM_sign_algo  v -> form { tmp { dkim  { k     = DKIM.sign_algos A.!! v                             } } } | ||||
|   DKIM_pubkey     v -> form { tmp { dkim  { p     =                      v                             } } } | ||||
|   DKIM_note       v -> form { tmp { dkim  { n     = Just                 v                             } } } | ||||
|  | @ -1,478 +1,15 @@ | |||
| module App.Type.ResourceRecord where | ||||
| 
 | ||||
| import Prelude (class Show, ($), (-), (<>)) | ||||
| -- import Data.String (toLower) | ||||
| import Data.Generic.Rep (class Generic) | ||||
| import App.Type.GenericSerialization (generic_serialization) | ||||
| import Data.Show.Generic (genericShow) | ||||
| 
 | ||||
| import Data.Array as A | ||||
| import Data.Maybe (Maybe(..), fromMaybe, maybe) | ||||
| import Data.Either (Either(..)) | ||||
| 
 | ||||
| import GenericParser.Parser as G | ||||
| import GenericParser.IPAddress as IPAddress | ||||
| import GenericParser.DomainParser.Common (DomainError) as DomainParser | ||||
| 
 | ||||
| import Utils (id, attach_id, remove_id) | ||||
| 
 | ||||
| import App.Validation.Email as Email | ||||
| 
 | ||||
| import Data.Codec.Argonaut (JsonCodec) | ||||
| import Data.Codec.Argonaut as CA | ||||
| import Data.Codec.Argonaut.Record as CAR | ||||
| import Data.Int (fromString) | ||||
| 
 | ||||
| import App.Type.ResourceRecord.DKIM as DKIM | ||||
| import App.Type.ResourceRecord.DMARC as DMARC | ||||
| import App.Type.ResourceRecord.SPF as SPF | ||||
| import App.Type.ResourceRecord.CAA as CAA | ||||
| 
 | ||||
| type RRId = Int | ||||
| 
 | ||||
| type ResourceRecord | ||||
|   = { rrtype   :: String | ||||
|     , rrid     :: RRId | ||||
|     , name     :: String | ||||
|     , ttl      :: Int | ||||
|     , target   :: String | ||||
|     , readonly :: Boolean | ||||
| 
 | ||||
|     -- MX (and SRV) specific entry. | ||||
|     , priority :: Maybe Int | ||||
| 
 | ||||
|     -- SRV specific entries. | ||||
|     , port     :: Maybe Int | ||||
|     , protocol :: Maybe SRVProtocol | ||||
|     , weight   :: Maybe Int | ||||
| 
 | ||||
|     -- SOA specific entries. | ||||
|     , mname   :: Maybe String | ||||
|     , rname   :: Maybe String | ||||
|     , serial  :: Maybe Int | ||||
|     , refresh :: Maybe Int | ||||
|     , retry   :: Maybe Int | ||||
|     , expire  :: Maybe Int | ||||
|     , minttl  :: Maybe Int | ||||
| 
 | ||||
|     , token   :: Maybe String | ||||
| 
 | ||||
|     -- SPF specific entries. | ||||
|     , v :: Maybe String -- Default: spf1 | ||||
|     , mechanisms :: Maybe (Array SPF.Mechanism) | ||||
|     , modifiers  :: Maybe (Array SPF.Modifier) | ||||
|     , q          :: Maybe SPF.Qualifier  -- Qualifier for default mechanism (`all`). | ||||
| 
 | ||||
|     , dkim :: Maybe DKIM.DKIM | ||||
|     , dmarc :: Maybe DMARC.DMARC | ||||
|     , caa :: Maybe CAA.CAA | ||||
|     } | ||||
| 
 | ||||
| codec :: JsonCodec ResourceRecord | ||||
| codec = CA.object "ResourceRecord" | ||||
|   (CAR.record | ||||
|     { rrtype:   CA.string | ||||
|     , rrid:     CA.int | ||||
|     , name:     CA.string | ||||
|     , ttl:      CA.int | ||||
|     , target:   CA.string | ||||
|     , readonly: CA.boolean | ||||
| 
 | ||||
|     -- MX (and SRV) specific entry. | ||||
|     , priority: CAR.optional CA.int | ||||
| 
 | ||||
|     -- SRV specific entries. | ||||
|     , port:     CAR.optional CA.int | ||||
|     , protocol: CAR.optional codecSRVProtocol | ||||
|     , weight:   CAR.optional CA.int | ||||
| 
 | ||||
|     -- SOA specific entries. | ||||
|     , mname:   CAR.optional CA.string | ||||
|     , rname:   CAR.optional CA.string | ||||
|     , serial:  CAR.optional CA.int | ||||
|     , refresh: CAR.optional CA.int | ||||
|     , retry:   CAR.optional CA.int | ||||
|     , expire:  CAR.optional CA.int | ||||
|     , minttl:  CAR.optional CA.int | ||||
| 
 | ||||
|     , token:   CAR.optional CA.string | ||||
| 
 | ||||
|     -- SPF specific entries. | ||||
|     , v:          CAR.optional CA.string | ||||
|     , mechanisms: CAR.optional (CA.array SPF.codecMechanism) | ||||
|     , modifiers:  CAR.optional (CA.array SPF.codecModifier) | ||||
|     , q:          CAR.optional SPF.codecQualifier | ||||
| 
 | ||||
|     , dkim:       CAR.optional DKIM.codec | ||||
|     , dmarc:      CAR.optional DMARC.codec | ||||
|     , caa:        CAR.optional CAA.codec | ||||
|     }) | ||||
| 
 | ||||
| emptyRR :: ResourceRecord | ||||
| emptyRR | ||||
|   = { rrid:         0 | ||||
|     , readonly:     false | ||||
|     , rrtype:       "" | ||||
|     , name:         "" | ||||
|     , ttl:          1800 | ||||
|     , target:       "" | ||||
| 
 | ||||
|     -- MX + SRV | ||||
|     , priority:     Nothing | ||||
| 
 | ||||
|     -- SRV | ||||
|     , port:         Nothing | ||||
|     , protocol:     Nothing | ||||
|     , weight:       Nothing | ||||
| 
 | ||||
|     -- SOA | ||||
|     , mname:        Nothing | ||||
|     , rname:        Nothing | ||||
|     , serial:       Nothing | ||||
|     , refresh:      Nothing | ||||
|     , retry:        Nothing | ||||
|     , expire:       Nothing | ||||
|     , minttl:       Nothing | ||||
| 
 | ||||
|     , token:        Nothing | ||||
| 
 | ||||
|     -- SPF specific entries. | ||||
|     , v:            Nothing | ||||
|     , mechanisms:   Nothing | ||||
|     , modifiers:    Nothing | ||||
|     , q:            Nothing | ||||
| 
 | ||||
|     , dkim:         Nothing | ||||
|     , dmarc:        Nothing | ||||
|     , caa:          Nothing | ||||
|     } | ||||
| 
 | ||||
| data SRVProtocol = TCP | UDP | ||||
| srv_protocols :: Array SRVProtocol | ||||
| srv_protocols = [TCP, UDP] | ||||
| srv_protocols_txt :: Array String | ||||
| srv_protocols_txt = ["tcp", "udp"] | ||||
| 
 | ||||
| derive instance genericSRVProtocol :: Generic SRVProtocol _ | ||||
| instance showSRVProtocol :: Show SRVProtocol where | ||||
|   show = genericShow | ||||
| 
 | ||||
| -- | Codec for just encoding a single value of type `Qualifier`. | ||||
| codecSRVProtocol :: CA.JsonCodec SRVProtocol | ||||
| codecSRVProtocol = CA.prismaticCodec "SRVProtocol" str_to_srv_protocol generic_serialization CA.string | ||||
| 
 | ||||
| str_to_srv_protocol :: String -> Maybe SRVProtocol | ||||
| str_to_srv_protocol = case _ of | ||||
|   "tcp" -> Just TCP | ||||
|   "udp" -> Just UDP | ||||
|   _ -> Nothing | ||||
| 
 | ||||
| data Field | ||||
|   = Domain    String | ||||
|   | TTL       String | ||||
|   | Target    String | ||||
|   | Priority  String | ||||
|   | Weight    String | ||||
|   | Port      String | ||||
|   | SPF_v          String | ||||
|   | SPF_mechanisms (Array SPF.Mechanism) | ||||
|   | SPF_modifiers  (Array SPF.Modifier) | ||||
|   | SPF_q          SPF.Qualifier | ||||
| 
 | ||||
|   | CAA_flag  String | ||||
|   | CAA_value String | ||||
| 
 | ||||
| -- | TMP: temporary stored values regarding specific records such as SPF, | ||||
| -- | DKIM and DMARC. | ||||
| type TMP = | ||||
|   { | ||||
|     -- SPF details. | ||||
|     spf :: { mechanism_q :: String | ||||
|            , mechanism_t :: String | ||||
|            , mechanism_v :: String | ||||
|            , modifier_t  :: String | ||||
|            , modifier_v  :: String | ||||
|            } | ||||
| 
 | ||||
|     -- DMARC details. | ||||
|   , dmarc_mail       :: String | ||||
|   , dmarc_mail_limit :: Maybe Int | ||||
|   , dmarc            :: DMARC.DMARC | ||||
| 
 | ||||
|     -- DKIM details. | ||||
|   , dkim :: DKIM.DKIM | ||||
|   } | ||||
| 
 | ||||
| -- | `Form` is the necessary state to modify a resource record. | ||||
| -- | It contains the currently manipulated record, detected errors, along with some temporary values. | ||||
| -- | FIXME: this form is messy AF and should be replaced. | ||||
| type Form = | ||||
|   { _rr                :: ResourceRecord | ||||
|   , _errors            :: Array Error | ||||
|   , _dmarc_mail_errors :: Array Email.Error | ||||
|   , _zonefile          :: Maybe String | ||||
|   , tmp                :: TMP | ||||
|   } | ||||
| 
 | ||||
| default_qualifier_str = "hard_fail" :: String | ||||
| default_caa = { flag: 0, tag: CAA.Issue, value: "letsencrypt.org" } :: CAA.CAA | ||||
| 
 | ||||
| default_rr :: AcceptedRRTypes -> String -> ResourceRecord | ||||
| default_rr t domain = | ||||
|   case t of | ||||
|     A     -> emptyRR { rrtype = "A", name = "server1", target = "192.0.2.1" } | ||||
|     AAAA  -> emptyRR { rrtype = "AAAA",  name = "server1",        target = "2001:db8::1" } | ||||
|     TXT   -> emptyRR { rrtype = "TXT",   name = "txt",            target = "some text" } | ||||
|     CNAME -> emptyRR { rrtype = "CNAME", name = "www",            target = "server1" } | ||||
|     NS    -> emptyRR { rrtype = "NS",    name = (domain <> "."),  target = "ns0.example.com." } | ||||
|     MX    -> emptyRR { rrtype = "MX",    name = "mail",           target = "server1", priority = Just 10 } | ||||
|     CAA   -> emptyRR { rrtype = "CAA",   name = "",               target = "",        caa = Just default_caa } | ||||
|     SRV   -> emptyRR { rrtype = "SRV",   name = "voip",           target = "server1" | ||||
|                      , port = Just 5061, weight = Just 100, priority = Just 10, protocol = Just TCP } | ||||
|     SPF   -> emptyRR { rrtype = "SPF",   name = "",               target = "" | ||||
|                      , mechanisms = Just default_mechanisms, q = Just SPF.HardFail } | ||||
|     DKIM  -> emptyRR { rrtype = "DKIM",  name = "default._domainkey", target = "" } | ||||
|     DMARC -> emptyRR { rrtype = "DMARC", name = "_dmarc",             target = "" } | ||||
|   where | ||||
|   default_mechanisms = maybe [] (\x -> [x]) $ SPF.to_mechanism "pass" "mx" "" | ||||
| 
 | ||||
| mkEmptyRRForm :: Form | ||||
| mkEmptyRRForm = | ||||
|   { | ||||
|   -- This is the state for the new RR modal. | ||||
|     _rr: default_rr A "" | ||||
|   -- List of errors within the form in new RR modal. | ||||
|   , _errors: [] | ||||
|   , _dmarc_mail_errors: [] | ||||
|   , _zonefile: Nothing | ||||
|   , tmp: { spf: { mechanism_q: "pass" | ||||
|                 , mechanism_t: "a" | ||||
|                 , mechanism_v: "" | ||||
|                 , modifier_t:  "redirect" | ||||
|                 , modifier_v:  "" | ||||
|                 } | ||||
|          , dkim:  DKIM.emptyDKIMRR | ||||
|          , dmarc: DMARC.emptyDMARCRR | ||||
|          , dmarc_mail: "" | ||||
|          , dmarc_mail_limit: Nothing | ||||
|          } | ||||
|   } | ||||
| 
 | ||||
| data RRUpdateValue | ||||
|   = CAA_tag Int | ||||
|   | SRV_Protocol Int | ||||
|   | SPF_Mechanism_q Int | ||||
|   | SPF_Mechanism_t Int | ||||
|   | SPF_Mechanism_v String | ||||
|   | SPF_Modifier_t  Int | ||||
|   | SPF_Modifier_v  String | ||||
|   | SPF_Qualifier   Int | ||||
| 
 | ||||
|   -- | Remove a SPF mechanism of the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_remove_mechanism Int | ||||
|   -- | Remove a SPF modifier of the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_remove_modifier Int | ||||
| 
 | ||||
|   -- | Add a SPF mechanism to the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_Mechanism_Add | ||||
|   -- | Add a SPF modifier to the currently modified (SPF) entry (see `_currentRR`). | ||||
|   | SPF_Modifier_Add | ||||
| 
 | ||||
|   -- | Change the temporary mail address for DMARC. | ||||
|   | DMARC_mail String | ||||
| 
 | ||||
|   -- | Change the temporary report size limit for DMARC. | ||||
|   | DMARC_mail_limit String | ||||
| 
 | ||||
|   -- | Change the requested report interval. | ||||
|   | DMARC_ri String | ||||
| 
 | ||||
|   -- | Add a new mail address to the DMARC rua list. | ||||
|   | DMARC_rua_Add | ||||
| 
 | ||||
|   -- | Add a new mail address to the DMARC ruf list. | ||||
|   | DMARC_ruf_Add | ||||
| 
 | ||||
|   -- | Remove a mail address of the DMARC rua list. | ||||
|   | DMARC_remove_rua Int | ||||
| 
 | ||||
|   -- | Remove a mail address of the DMARC ruf list. | ||||
|   | DMARC_remove_ruf Int | ||||
| 
 | ||||
|   | DMARC_policy Int | ||||
|   | DMARC_sp_policy Int | ||||
|   | DMARC_adkim Int | ||||
|   | DMARC_aspf Int | ||||
|   | DMARC_pct  String | ||||
|   | DMARC_fo Int | ||||
| 
 | ||||
|   | DKIM_hash_algo Int | ||||
|   | DKIM_sign_algo Int | ||||
|   | DKIM_pubkey    String | ||||
|   | DKIM_note      String | ||||
| 
 | ||||
| update_form :: Form -> RRUpdateValue -> Form | ||||
| update_form form new_field_value = | ||||
|   case new_field_value of | ||||
|   CAA_tag         v -> | ||||
|     let new_tag = fromMaybe CAA.Issue $ CAA.tags A.!! v | ||||
|         new_value = case new_tag of | ||||
|           CAA.Issue -> "letsencrypt.org" | ||||
|           CAA.ContactEmail -> "contact@example.com" | ||||
|           CAA.ContactPhone -> "0203040506" | ||||
|           _                -> "" | ||||
|         new_caa = (fromMaybe default_caa form._rr.caa) { tag = new_tag, value = new_value } | ||||
|     in form { _rr { caa = Just new_caa } } | ||||
| 
 | ||||
|   SRV_Protocol v    -> form { _rr { protocol = srv_protocols A.!! v } } | ||||
|   SPF_Mechanism_q v -> form { tmp { spf { mechanism_q = maybe "pass"     id $ SPF.qualifier_types A.!! v  }}} | ||||
|   SPF_Mechanism_t v -> form { tmp { spf { mechanism_t = maybe "a"        id $ SPF.mechanism_types A.!! v  }}} | ||||
|   SPF_Mechanism_v v -> form { tmp { spf { mechanism_v = v                                                 }}} | ||||
|   SPF_Modifier_t v  -> form { tmp { spf { modifier_t  = maybe "redirect" id $ SPF.modifier_types  A.!! v  }}} | ||||
|   SPF_Modifier_v v  -> form { tmp { spf { modifier_v  = v                                                 }}} | ||||
|   SPF_Qualifier v   -> form { _rr { q  = SPF.qualifiers A.!! v                                             }} | ||||
|   SPF_remove_mechanism i -> | ||||
|     form { _rr { mechanisms = case form._rr.mechanisms of | ||||
|                    Just ms -> Just (remove_id i $ attach_id 0 ms) | ||||
|                    Nothing -> Nothing | ||||
|                } } | ||||
|   SPF_remove_modifier i -> | ||||
|     form { _rr { modifiers = case form._rr.modifiers of | ||||
|                    Just ms -> Just (remove_id i $ attach_id 0 ms) | ||||
|                    Nothing -> Nothing | ||||
|                } } | ||||
| 
 | ||||
|   SPF_Mechanism_Add -> | ||||
|     let m = form._rr.mechanisms | ||||
|         m_q = form.tmp.spf.mechanism_q | ||||
|         m_t = form.tmp.spf.mechanism_t | ||||
|         m_v = form.tmp.spf.mechanism_v | ||||
|         new_list_of_mechanisms = maybe [] id m <> maybe [] (\x -> [x]) (SPF.to_mechanism m_q m_t m_v) | ||||
|         new_value = case new_list_of_mechanisms of | ||||
|           [] -> Nothing | ||||
|           v  -> Just v | ||||
|     in form { _rr { mechanisms = new_value }} | ||||
| 
 | ||||
|   SPF_Modifier_Add -> | ||||
|     let m = form._rr.modifiers | ||||
|         m_t = form.tmp.spf.modifier_t | ||||
|         m_v = form.tmp.spf.modifier_v | ||||
|         new_list_of_modifiers = maybe [] id m <> maybe [] (\x -> [x]) (SPF.to_modifier m_t m_v) | ||||
|         new_value = case new_list_of_modifiers of | ||||
|           [] -> Nothing | ||||
|           v  -> Just v | ||||
|     in form { _rr { modifiers = new_value }} | ||||
| 
 | ||||
|   DMARC_mail       v -> form { tmp { dmarc_mail = v } } | ||||
|   DMARC_mail_limit v -> form { tmp { dmarc_mail_limit = Just $ fromMaybe 0 $ fromString v } } | ||||
|   DMARC_ri         v -> form { tmp { dmarc { ri = fromString v } } } | ||||
|   DMARC_rua_Add -> | ||||
|     case Email.email form.tmp.dmarc_mail of | ||||
|       Left errors -> form { _dmarc_mail_errors = errors } | ||||
|       Right _     -> | ||||
|         let current_ruas = fromMaybe [] form.tmp.dmarc.rua | ||||
|             new_list = current_ruas <> [ {mail: form.tmp.dmarc_mail, limit: form.tmp.dmarc_mail_limit} ] | ||||
|         in form { tmp { dmarc { rua = Just new_list }}} | ||||
| 
 | ||||
|   DMARC_ruf_Add -> | ||||
|     case Email.email form.tmp.dmarc_mail of | ||||
|       Left errors -> form { _dmarc_mail_errors = errors } | ||||
|       Right _     -> | ||||
|         let current_rufs = fromMaybe [] form.tmp.dmarc.ruf | ||||
|             new_list = current_rufs <> [ {mail: form.tmp.dmarc_mail, limit: form.tmp.dmarc_mail_limit} ] | ||||
|         in form { tmp { dmarc { ruf = Just new_list }}} | ||||
| 
 | ||||
|   DMARC_remove_rua i -> | ||||
|     let current_ruas = fromMaybe [] form.tmp.dmarc.rua | ||||
|         new_value = case (remove_id i $ attach_id 0 current_ruas) of | ||||
|           [] -> Nothing | ||||
|           v -> Just v | ||||
|     in form { tmp { dmarc { rua = new_value } } } | ||||
| 
 | ||||
|   DMARC_remove_ruf i -> | ||||
|     let current_rufs = fromMaybe [] form.tmp.dmarc.ruf | ||||
|         new_value = case (remove_id i $ attach_id 0 current_rufs) of | ||||
|           [] -> Nothing | ||||
|           v -> Just v | ||||
|     in form { tmp { dmarc { ruf = new_value } } } | ||||
| 
 | ||||
|   DMARC_policy    v -> form { tmp { dmarc { p     = fromMaybe DMARC.None $ DMARC.policies A.!! v       } } } | ||||
|   DMARC_sp_policy v -> form { tmp { dmarc { sp    =                        DMARC.policies A.!! (v - 1) } } } | ||||
|   DMARC_adkim     v -> form { tmp { dmarc { adkim = DMARC.consistency_policies A.!! (v - 1)            } } } | ||||
|   DMARC_aspf      v -> form { tmp { dmarc { aspf  = DMARC.consistency_policies A.!! (v - 1)            } } } | ||||
|   DMARC_pct       v -> form { tmp { dmarc { pct   = Just $ fromMaybe 100 (fromString v)                } } } | ||||
|   DMARC_fo        v -> form { tmp { dmarc { fo    = DMARC.report_occasions A.!! (v - 1)                } } } | ||||
|   DKIM_hash_algo  v -> form { tmp { dkim  { h     = DKIM.hash_algos A.!! v                             } } } | ||||
|   DKIM_sign_algo  v -> form { tmp { dkim  { k     = DKIM.sign_algos A.!! v                             } } } | ||||
|   DKIM_pubkey     v -> form { tmp { dkim  { p     =                      v                             } } } | ||||
|   DKIM_note       v -> form { tmp { dkim  { n     = Just                 v                             } } } | ||||
| 
 | ||||
| -- | Errors that might be catched in for the form upon validation (`App.Validation.DNS`). | ||||
| -- | | ||||
| -- | **History:** | ||||
| -- | The module once used dedicated types for each type of RR. | ||||
| -- | That comes with several advantages. | ||||
| -- | First, type verification was a thing, and function were dedicated to a certain type of record. | ||||
| -- | Second, these dedicated types used strings for their fields, | ||||
| -- | which simplifies the typing when dealing with forms. | ||||
| -- | Finally, the validation was a way to convert dedicated types (used in forms) | ||||
| -- | to the general type (used for network serialization). | ||||
| -- | This ensures each resource record is verified before being sent to `dnsmanagerd`. | ||||
| -- | | ||||
| -- | The problem is that, with dedicated types, you are then required to have dedicated functions. | ||||
| -- | Conversion functions are also required. | ||||
| -- | | ||||
| -- | Maybe the code will change again in the future, but for now it will be enough. | ||||
| 
 | ||||
| data Error | ||||
|   = UNKNOWN | ||||
|   | VEIPv4 (G.Error IPAddress.IPv4Error) | ||||
|   | VEIPv6 (G.Error IPAddress.IPv6Error) | ||||
|   | VEName (G.Error DomainParser.DomainError) | ||||
|   | VETTL Int Int Int | ||||
|   | VETXT (G.Error TXTError) | ||||
|   | VECNAME (G.Error DomainParser.DomainError) | ||||
|   | VENS (G.Error DomainParser.DomainError) | ||||
|   | VEMX (G.Error DomainParser.DomainError) | ||||
|   | VEPriority Int Int Int | ||||
|   | VESRV (G.Error DomainParser.DomainError) | ||||
|   | VEPort Int Int Int | ||||
|   | VEWeight Int Int Int | ||||
|   | VEDMARCpct Int Int Int | ||||
|   | VEDMARCri  Int Int Int | ||||
| 
 | ||||
|   | VECAAflag  Int Int Int -- CAA flag should be between 0 and 255 (1 byte). | ||||
| 
 | ||||
|   -- SPF | ||||
|   | VESPFMechanismName (G.Error DomainParser.DomainError) | ||||
|   | VESPFMechanismIPv4 (G.Error IPAddress.IPv4Error) | ||||
|   | VESPFMechanismIPv6 (G.Error IPAddress.IPv6Error) | ||||
| 
 | ||||
|   | VESPFModifierName (G.Error DomainParser.DomainError) | ||||
| 
 | ||||
|   | DKIMInvalidKeySize Int Int | ||||
| 
 | ||||
| -- | The application accepts to add a few new entry types in a DNS zone. | ||||
| -- | Each resource record has a specific form, with dedicated inputs and | ||||
| -- | dedicated validation. | ||||
| data AcceptedRRTypes | ||||
|   = A | ||||
|   | AAAA | ||||
|   | TXT | ||||
|   | CNAME | ||||
|   | NS | ||||
|   | MX | ||||
|   | CAA | ||||
|   | SRV | ||||
|   | SPF | ||||
|   | DKIM | ||||
|   | DMARC | ||||
| 
 | ||||
| derive instance genericAcceptedRRTypes :: Generic AcceptedRRTypes _ | ||||
| 
 | ||||
| instance showAcceptedRRTypes :: Show AcceptedRRTypes where | ||||
|   show = genericShow | ||||
| 
 | ||||
| data TXTError | ||||
|   = TXTInvalidCharacter | ||||
|   | TXTTooLong Int Int -- max current | ||||
| module App.Type.ResourceRecord | ||||
|   ( module App.Type.Error.ResourceRecord | ||||
|   , module App.Type.Form.ResourceRecord | ||||
|   , module App.Type.ResourceRecord.AcceptedRRTypes | ||||
|   , module App.Type.ResourceRecord.ResourceRecord | ||||
|   ) where | ||||
| 
 | ||||
| import App.Type.Error.ResourceRecord | ||||
| import App.Type.Form.ResourceRecord (Field(..), Form, RRUpdateValue(..), TMP, mkEmptyRRForm, update_form) | ||||
| import App.Type.ResourceRecord.AcceptedRRTypes (AcceptedRRTypes(..)) | ||||
| 
 | ||||
| import App.Type.ResourceRecord.ResourceRecord (RRId, ResourceRecord, SRVProtocol(..) | ||||
|                                               , codec, codecSRVProtocol, default_caa | ||||
|                                               , default_qualifier_str, default_rr | ||||
|                                               , emptyRR, srv_protocols, srv_protocols_txt, str_to_srv_protocol) | ||||
|  |  | |||
							
								
								
									
										27
									
								
								src/App/Type/ResourceRecord/AcceptedRRTypes.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/App/Type/ResourceRecord/AcceptedRRTypes.purs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| module App.Type.ResourceRecord.AcceptedRRTypes where | ||||
| 
 | ||||
| import Prelude (class Show, ($), (-), (<>)) | ||||
| 
 | ||||
| import Data.Generic.Rep (class Generic) | ||||
| import Data.Show.Generic (genericShow) | ||||
| 
 | ||||
| -- | The application accepts to add a few new entry types in a DNS zone. | ||||
| -- | Each resource record has a specific form, with dedicated inputs and | ||||
| -- | dedicated validation. | ||||
| data AcceptedRRTypes | ||||
|   = A | ||||
|   | AAAA | ||||
|   | TXT | ||||
|   | CNAME | ||||
|   | NS | ||||
|   | MX | ||||
|   | CAA | ||||
|   | SRV | ||||
|   | SPF | ||||
|   | DKIM | ||||
|   | DMARC | ||||
| 
 | ||||
| derive instance genericAcceptedRRTypes :: Generic AcceptedRRTypes _ | ||||
| 
 | ||||
| instance showAcceptedRRTypes :: Show AcceptedRRTypes where | ||||
|   show = genericShow | ||||
							
								
								
									
										182
									
								
								src/App/Type/ResourceRecord/ResourceRecord.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/App/Type/ResourceRecord/ResourceRecord.purs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,182 @@ | |||
| module App.Type.ResourceRecord.ResourceRecord where | ||||
| 
 | ||||
| import Prelude (class Show, ($), (<>)) | ||||
| 
 | ||||
| -- import Data.String (toLower) | ||||
| import Data.Generic.Rep (class Generic) | ||||
| import App.Type.GenericSerialization (generic_serialization) | ||||
| import Data.Show.Generic (genericShow) | ||||
| 
 | ||||
| import Data.Maybe (Maybe(..), maybe) | ||||
| 
 | ||||
| import Data.Codec.Argonaut (JsonCodec) | ||||
| import Data.Codec.Argonaut as CA | ||||
| import Data.Codec.Argonaut.Record as CAR | ||||
| 
 | ||||
| import App.Type.ResourceRecord.AcceptedRRTypes (AcceptedRRTypes(..)) | ||||
| 
 | ||||
| import App.Type.ResourceRecord.CAA as CAA | ||||
| import App.Type.ResourceRecord.DKIM as DKIM | ||||
| import App.Type.ResourceRecord.DMARC as DMARC | ||||
| import App.Type.ResourceRecord.SPF as SPF | ||||
| 
 | ||||
| type RRId = Int | ||||
| 
 | ||||
| type ResourceRecord | ||||
|   = { rrtype   :: String | ||||
|     , rrid     :: RRId | ||||
|     , name     :: String | ||||
|     , ttl      :: Int | ||||
|     , target   :: String | ||||
|     , readonly :: Boolean | ||||
| 
 | ||||
|     -- MX (and SRV) specific entry. | ||||
|     , priority :: Maybe Int | ||||
| 
 | ||||
|     -- SRV specific entries. | ||||
|     , port     :: Maybe Int | ||||
|     , protocol :: Maybe SRVProtocol | ||||
|     , weight   :: Maybe Int | ||||
| 
 | ||||
|     -- SOA specific entries. | ||||
|     , mname   :: Maybe String | ||||
|     , rname   :: Maybe String | ||||
|     , serial  :: Maybe Int | ||||
|     , refresh :: Maybe Int | ||||
|     , retry   :: Maybe Int | ||||
|     , expire  :: Maybe Int | ||||
|     , minttl  :: Maybe Int | ||||
| 
 | ||||
|     , token   :: Maybe String | ||||
| 
 | ||||
|     -- SPF specific entries. | ||||
|     , v :: Maybe String -- Default: spf1 | ||||
|     , mechanisms :: Maybe (Array SPF.Mechanism) | ||||
|     , modifiers  :: Maybe (Array SPF.Modifier) | ||||
|     , q          :: Maybe SPF.Qualifier  -- Qualifier for default mechanism (`all`). | ||||
| 
 | ||||
|     , dkim :: Maybe DKIM.DKIM | ||||
|     , dmarc :: Maybe DMARC.DMARC | ||||
|     , caa :: Maybe CAA.CAA | ||||
|     } | ||||
| 
 | ||||
| codec :: JsonCodec ResourceRecord | ||||
| codec = CA.object "ResourceRecord" | ||||
|   (CAR.record | ||||
|     { rrtype:   CA.string | ||||
|     , rrid:     CA.int | ||||
|     , name:     CA.string | ||||
|     , ttl:      CA.int | ||||
|     , target:   CA.string | ||||
|     , readonly: CA.boolean | ||||
| 
 | ||||
|     -- MX (and SRV) specific entry. | ||||
|     , priority: CAR.optional CA.int | ||||
| 
 | ||||
|     -- SRV specific entries. | ||||
|     , port:     CAR.optional CA.int | ||||
|     , protocol: CAR.optional codecSRVProtocol | ||||
|     , weight:   CAR.optional CA.int | ||||
| 
 | ||||
|     -- SOA specific entries. | ||||
|     , mname:   CAR.optional CA.string | ||||
|     , rname:   CAR.optional CA.string | ||||
|     , serial:  CAR.optional CA.int | ||||
|     , refresh: CAR.optional CA.int | ||||
|     , retry:   CAR.optional CA.int | ||||
|     , expire:  CAR.optional CA.int | ||||
|     , minttl:  CAR.optional CA.int | ||||
| 
 | ||||
|     , token:   CAR.optional CA.string | ||||
| 
 | ||||
|     -- SPF specific entries. | ||||
|     , v:          CAR.optional CA.string | ||||
|     , mechanisms: CAR.optional (CA.array SPF.codecMechanism) | ||||
|     , modifiers:  CAR.optional (CA.array SPF.codecModifier) | ||||
|     , q:          CAR.optional SPF.codecQualifier | ||||
| 
 | ||||
|     , dkim:       CAR.optional DKIM.codec | ||||
|     , dmarc:      CAR.optional DMARC.codec | ||||
|     , caa:        CAR.optional CAA.codec | ||||
|     }) | ||||
| 
 | ||||
| emptyRR :: ResourceRecord | ||||
| emptyRR | ||||
|   = { rrid:         0 | ||||
|     , readonly:     false | ||||
|     , rrtype:       "" | ||||
|     , name:         "" | ||||
|     , ttl:          1800 | ||||
|     , target:       "" | ||||
| 
 | ||||
|     -- MX + SRV | ||||
|     , priority:     Nothing | ||||
| 
 | ||||
|     -- SRV | ||||
|     , port:         Nothing | ||||
|     , protocol:     Nothing | ||||
|     , weight:       Nothing | ||||
| 
 | ||||
|     -- SOA | ||||
|     , mname:        Nothing | ||||
|     , rname:        Nothing | ||||
|     , serial:       Nothing | ||||
|     , refresh:      Nothing | ||||
|     , retry:        Nothing | ||||
|     , expire:       Nothing | ||||
|     , minttl:       Nothing | ||||
| 
 | ||||
|     , token:        Nothing | ||||
| 
 | ||||
|     -- SPF specific entries. | ||||
|     , v:            Nothing | ||||
|     , mechanisms:   Nothing | ||||
|     , modifiers:    Nothing | ||||
|     , q:            Nothing | ||||
| 
 | ||||
|     , dkim:         Nothing | ||||
|     , dmarc:        Nothing | ||||
|     , caa:          Nothing | ||||
|     } | ||||
| 
 | ||||
| data SRVProtocol = TCP | UDP | ||||
| srv_protocols :: Array SRVProtocol | ||||
| srv_protocols = [TCP, UDP] | ||||
| srv_protocols_txt :: Array String | ||||
| srv_protocols_txt = ["tcp", "udp"] | ||||
| 
 | ||||
| derive instance genericSRVProtocol :: Generic SRVProtocol _ | ||||
| instance showSRVProtocol :: Show SRVProtocol where | ||||
|   show = genericShow | ||||
| 
 | ||||
| -- | Codec for just encoding a single value of type `Qualifier`. | ||||
| codecSRVProtocol :: CA.JsonCodec SRVProtocol | ||||
| codecSRVProtocol = CA.prismaticCodec "SRVProtocol" str_to_srv_protocol generic_serialization CA.string | ||||
| 
 | ||||
| str_to_srv_protocol :: String -> Maybe SRVProtocol | ||||
| str_to_srv_protocol = case _ of | ||||
|   "tcp" -> Just TCP | ||||
|   "udp" -> Just UDP | ||||
|   _ -> Nothing | ||||
| 
 | ||||
| default_qualifier_str = "hard_fail" :: String | ||||
| default_caa = { flag: 0, tag: CAA.Issue, value: "letsencrypt.org" } :: CAA.CAA | ||||
| 
 | ||||
| default_rr :: AcceptedRRTypes -> String -> ResourceRecord | ||||
| default_rr t domain = | ||||
|   case t of | ||||
|     A     -> emptyRR { rrtype = "A", name = "server1", target = "192.0.2.1" } | ||||
|     AAAA  -> emptyRR { rrtype = "AAAA",  name = "server1",        target = "2001:db8::1" } | ||||
|     TXT   -> emptyRR { rrtype = "TXT",   name = "txt",            target = "some text" } | ||||
|     CNAME -> emptyRR { rrtype = "CNAME", name = "www",            target = "server1" } | ||||
|     NS    -> emptyRR { rrtype = "NS",    name = (domain <> "."),  target = "ns0.example.com." } | ||||
|     MX    -> emptyRR { rrtype = "MX",    name = "mail",           target = "server1", priority = Just 10 } | ||||
|     CAA   -> emptyRR { rrtype = "CAA",   name = "",               target = "",        caa = Just default_caa } | ||||
|     SRV   -> emptyRR { rrtype = "SRV",   name = "voip",           target = "server1" | ||||
|                      , port = Just 5061, weight = Just 100, priority = Just 10, protocol = Just TCP } | ||||
|     SPF   -> emptyRR { rrtype = "SPF",   name = "",               target = "" | ||||
|                      , mechanisms = Just default_mechanisms, q = Just SPF.HardFail } | ||||
|     DKIM  -> emptyRR { rrtype = "DKIM",  name = "default._domainkey", target = "" } | ||||
|     DMARC -> emptyRR { rrtype = "DMARC", name = "_dmarc",             target = "" } | ||||
|   where | ||||
|   default_mechanisms = maybe [] (\x -> [x]) $ SPF.to_mechanism "pass" "mx" "" | ||||
		Loading…
	
	Add table
		
		Reference in a new issue