spec doc clarification
[pdns-pipe-nmc.git] / NmcDom.hs
1 {-# LANGUAGE OverloadedStrings #-}
2
3 module NmcDom   ( NmcDom(..)
4                 , NmcRRService(..)
5                 , emptyNmcDom
6                 , seedNmcDom
7                 , descendNmcDom
8                 ) where
9
10 import Prelude hiding (length)
11 import Data.ByteString.Lazy (ByteString)
12 import Data.Text (Text, unpack)
13 import Data.List as L (union)
14 import Data.List.Split
15 import Data.Char
16 import Data.Map as M (Map, lookup, delete, size, unionWith)
17 import Data.Vector (toList,(!),length, singleton)
18 import Control.Applicative ((<$>), (<*>), empty, pure)
19 import Data.Aeson
20
21 import qualified Data.HashMap.Strict as H
22 import Data.Aeson.Types
23
24 -- Variant of Aeson's `.:?` that interprets a String as a
25 -- single-element list, so it is possible to have either
26 --      "ip":["1.2.3.4"]
27 -- or
28 --      "ip":"1.2.3.4"
29 -- with the same result.
30 (.:/) :: (FromJSON a) => Object -> Text -> Parser (Maybe a)
31 obj .:/ key = case H.lookup key obj of
32                Nothing -> pure Nothing
33                Just v  -> case v of
34                         String s -> parseJSON $ Array (singleton v)
35                         _        -> parseJSON v
36
37 class Mergeable a where
38         merge :: a -> a -> a -- bias towads second arg
39
40 instance (Ord k, Mergeable a) => Mergeable (Map k a) where
41         merge mx my = M.unionWith merge my mx
42
43 -- instance Mergeable String where
44 --         merge _ b = b
45
46 instance Mergeable Value where
47         merge _ b = b
48
49 instance Mergeable a => Mergeable (Maybe a) where
50         merge (Just x) (Just y) = Just (merge x y)
51         merge Nothing  (Just y) = Just y
52         merge (Just x) Nothing  = Just x
53         merge Nothing  Nothing  = Nothing
54
55 instance Eq a => Mergeable [a] where
56         merge xs ys = L.union xs ys
57
58 data NmcRRService = NmcRRService
59                         { srvName       :: String
60                         , srvProto      :: String
61                         , srvPrio       :: Int
62                         , srvWeight     :: Int
63                         , srvPort       :: Int
64                         , srvHost       :: String
65                         } deriving (Show, Eq)
66
67 instance FromJSON NmcRRService where
68         parseJSON (Array a) =
69                 if length a == 6 then NmcRRService
70                         <$> parseJSON (a ! 0)
71                         <*> parseJSON (a ! 1)
72                         <*> parseJSON (a ! 2)
73                         <*> parseJSON (a ! 3)
74                         <*> parseJSON (a ! 4)
75                         <*> parseJSON (a ! 5)
76                 else empty
77         parseJSON _ = empty
78
79 instance Mergeable NmcRRService where
80         merge _ b = b
81
82 data NmcRRI2p = NmcRRI2p
83                         { i2pDestination :: String
84                         , i2pName        :: String
85                         , i2pB32         :: String
86                         } deriving (Show, Eq)
87
88 instance FromJSON NmcRRI2p where
89         parseJSON (Object o) = NmcRRI2p
90                 <$> o .: "destination"
91                 <*> o .: "name"
92                 <*> o .: "b32"
93         parseJSON _ = empty
94
95 instance Mergeable NmcRRI2p where
96         merge _ b = b
97
98 data NmcRRTls = NmcRRTls
99                         { tlsMatchType  :: Int -- 0:exact 1:sha256 2:sha512
100                         , tlsMatchValue :: String
101                         , tlsIncSubdoms :: Int -- 1:enforce on subdoms 0:no
102                         } deriving (Show, Eq)
103
104 instance FromJSON NmcRRTls where
105         parseJSON (Array a) =
106                 if length a == 3 then NmcRRTls
107                         <$> parseJSON (a ! 0)
108                         <*> parseJSON (a ! 1)
109                         <*> parseJSON (a ! 2)
110                 else empty
111         parseJSON _ = empty
112
113 instance Mergeable NmcRRTls where
114         merge _ b = b
115
116 data NmcRRDs = NmcRRDs
117                         { dsKeyTag      :: Int
118                         , dsAlgo        :: Int
119                         , dsHashType    :: Int
120                         , dsHashValue   :: String
121                         } deriving (Show, Eq)
122
123 instance FromJSON NmcRRDs where
124         parseJSON (Array a) =
125                 if length a == 4 then NmcRRDs
126                         <$> parseJSON (a ! 0)
127                         <*> parseJSON (a ! 1)
128                         <*> parseJSON (a ! 2)
129                         <*> parseJSON (a ! 3)
130                 else empty
131         parseJSON _ = empty
132
133 instance Mergeable NmcRRDs where
134         merge _ b = b
135
136 data NmcDom = NmcDom    { domService     :: Maybe [NmcRRService]
137                         , domIp          :: Maybe [String]
138                         , domIp6         :: Maybe [String]
139                         , domTor         :: Maybe String
140                         , domI2p         :: Maybe NmcRRI2p
141                         , domFreenet     :: Maybe String
142                         , domAlias       :: Maybe String
143                         , domTranslate   :: Maybe String
144                         , domEmail       :: Maybe String
145                         , domLoc         :: Maybe String
146                         , domInfo        :: Maybe Value
147                         , domNs          :: Maybe [String]
148                         , domDelegate    :: Maybe String
149                         , domImport      :: Maybe String
150                         , domMap         :: Maybe (Map String NmcDom)
151                         , domFingerprint :: Maybe [String]
152                         , domTls         :: Maybe (Map String
153                                                     (Map String [NmcRRTls]))
154                         , domDs          :: Maybe [NmcRRDs]
155                         , domMx          :: Maybe [String] -- Synthetic
156                         } deriving (Show, Eq)
157
158 instance FromJSON NmcDom where
159         -- Wherever we expect a domain object, there may be a string
160         -- containing IPv4 address. Interpret it as such.
161         -- Question: shall we try to recognize IPv6 addresses too?
162         parseJSON (String s) =
163                  return $ if isIPv4 s'
164                             then emptyNmcDom { domIp = Just [s'] }
165                             else emptyNmcDom
166                           where
167                             s' = unpack s
168                             isIPv4 x = all isNibble $ splitOn "." x
169                             isNibble x =
170                               if all isDigit x then (read x :: Int) < 256
171                               else False
172         parseJSON (Object o) = NmcDom
173                 <$> o .:? "service"
174                 <*> o .:/ "ip"
175                 <*> o .:/ "ip6"
176                 <*> o .:? "tor"
177                 <*> o .:? "i2p"
178                 <*> o .:? "freenet"
179                 <*> o .:? "alias"
180                 <*> o .:? "translate"
181                 <*> o .:? "email"
182                 <*> o .:? "loc"
183                 <*> o .:? "info"
184                 <*> o .:/ "ns"
185                 <*> o .:? "delegate"
186                 <*> o .:? "import"
187                 <*> o .:? "map"
188                 <*> o .:/ "fingerprint"
189                 <*> o .:? "tls"
190                 <*> o .:? "ds"
191                 <*> return Nothing -- domMx not parsed
192         parseJSON _ = empty
193
194 instance Mergeable NmcDom where
195         merge sub dom = dom     { domService =     mergelm domService
196                                 , domIp =          mergelm domIp
197                                 , domIp6 =         mergelm domIp6
198                                 , domTor =         choose  domTor
199                                 , domI2p =         mergelm domI2p
200                                 , domFreenet =     choose  domFreenet
201                                 , domAlias =       choose  domAlias
202                                 , domTranslate =   choose  domTranslate
203                                 , domEmail =       choose  domEmail
204                                 , domLoc =         choose  domLoc
205                                 , domInfo =        mergelm domInfo
206                                 , domNs =          mergelm domNs
207                                 , domDelegate =    mergelm domDelegate
208                                 , domImport =      choose  domImport
209                                 , domMap =         mergelm domMap
210                                 , domFingerprint = mergelm domFingerprint
211                                 , domTls =         mergelm domTls
212                                 , domDs =          mergelm domDs
213                                 , domMx =          mergelm domMx
214                                 }
215           where
216                 mergelm x = merge (x sub) (x dom)
217 -- Because it is not possible to define instance of merge for Strings,
218 -- we have to treat string elements separately, otherwise strings are
219 -- 'unioned' along with the rest of lists. Ugly, but alternatives are worse.
220                 choose field = case field dom of
221                         Nothing -> field sub
222                         Just x  -> Just x
223
224
225 emptyNmcDom = NmcDom Nothing Nothing Nothing Nothing Nothing Nothing
226                      Nothing Nothing Nothing Nothing Nothing Nothing
227                      Nothing Nothing Nothing Nothing Nothing Nothing
228                      Nothing
229
230 -- | Perform query and return error string or parsed domain object
231 queryNmcDom ::
232   (String -> IO (Either String ByteString)) -- ^ query operation action
233   -> String                                 -- ^ key
234   -> IO (Either String NmcDom)              -- ^ error string or domain
235 queryNmcDom queryOp key = do
236   l <- queryOp key
237   case l of
238     Left estr -> return $ Left estr
239     Right str -> case decode str :: Maybe NmcDom of
240       Nothing  -> return $ Left $ "Unparseable value: " ++ (show str)
241       Just dom -> return $ Right dom
242
243 -- | Try to fetch "import" object and merge it into the base domain
244 --   Original "import" element is removed, but new imports from the
245 --   imported objects are processed recursively until there are none.
246 mergeImport ::
247   (String -> IO (Either String ByteString)) -- ^ query operation action
248   -> Int                                    -- ^ recursion counter
249   -> NmcDom                                 -- ^ base domain
250   -> IO (Either String NmcDom)              -- ^ result with merged import
251 mergeImport queryOp depth base = do
252   let
253     mbase = mergeSelf base
254     base' = mbase {domImport = Nothing}
255   -- print base
256   if depth <= 0 then return $ Left "Nesting of imports is too deep"
257   else case domImport mbase of
258     Nothing  -> return $ Right base'
259     Just key -> do
260       sub <- queryNmcDom queryOp key
261       case sub of
262         Left  e    -> return $ Left e
263         Right sub' -> mergeImport queryOp (depth - 1) $ sub' `merge` base'
264
265 -- | If there is an element in the map with key "", merge the contents
266 --   and remove this element. Do this recursively.
267 mergeSelf :: NmcDom -> NmcDom
268 mergeSelf base =
269   let
270     map   = domMap base
271     base' = base {domMap = removeSelf map}
272     removeSelf Nothing    = Nothing
273     removeSelf (Just map) = if size map' == 0 then Nothing else Just map'
274       where map' = M.delete "" map
275   in
276     case map of
277       Nothing   -> base'
278       Just map' ->
279         case M.lookup "" map' of
280           Nothing  -> base'
281           Just sub -> (mergeSelf sub) `merge` base'
282         -- recursion depth limited by the size of the record
283
284 -- | SRV case - remove everyting and filter SRV records
285 normalizeSrv :: String -> String -> NmcDom -> NmcDom
286 normalizeSrv serv proto dom =
287   emptyNmcDom {domService = fmap (filter needed) (domService dom)}
288     where
289       needed r = srvName r == serv && srvProto r == proto
290
291 -- | Presence of some elements require removal of some others
292 normalizeDom :: NmcDom -> NmcDom
293 normalizeDom dom = foldr id dom [ srvNormalizer
294                                 , translateNormalizer
295                                 , nsNormalizer
296                                 ]
297   where
298     nsNormalizer dom = case domNs dom of
299       Nothing  -> dom
300       Just ns  -> emptyNmcDom { domNs = domNs dom, domEmail = domEmail dom }
301     translateNormalizer dom = case domTranslate dom of
302       Nothing  -> dom
303       Just tr  -> dom { domMap = Nothing }
304     srvNormalizer dom = dom { domService = Nothing, domMx = makemx }
305       where
306         makemx = case domService dom of
307           Nothing  -> Nothing
308           Just svl -> Just $ map makerec (filter needed svl)
309             where
310               needed sr = srvName sr == "smtp"
311                         && srvProto sr == "tcp"
312                         && srvPort sr == 25
313               makerec sr = (show (srvPrio sr)) ++ " " ++ (srvHost sr)
314
315 -- | Merge imports and Selfs and follow the maps tree to get dom
316 descendNmcDom ::
317   (String -> IO (Either String ByteString)) -- ^ query operation action
318   -> [String]                               -- ^ subdomain chain
319   -> NmcDom                                 -- ^ base domain
320   -> IO (Either String NmcDom)              -- ^ fully processed result
321 descendNmcDom queryOp subdom base = do
322   base' <- mergeImport queryOp 10 base
323   case subdom of
324     []   -> return $ fmap normalizeDom base'
325     -- A hack to handle SRV records: don't descend if ["_prot","_serv"]
326     [('_':p),('_':s)] -> return $ fmap (normalizeSrv s p) base'
327     d:ds ->
328       case base' of
329         Left err     -> return base'
330         Right base'' ->
331           case domMap base'' of
332             Nothing  -> return $ Right emptyNmcDom
333             Just map ->
334               case M.lookup d map of
335                 Nothing  -> return $ Right emptyNmcDom
336                 Just sub -> descendNmcDom queryOp ds sub
337
338 -- | Initial NmcDom populated with "import" only, suitable for "descend"
339 seedNmcDom ::
340   String        -- ^ domain key (without namespace prefix)
341   -> NmcDom     -- ^ resulting seed domain
342 seedNmcDom dn = emptyNmcDom { domImport = Just ("d/" ++ dn)}