1 {-# LANGUAGE OverloadedStrings #-}
3 module NmcDom ( NmcDom(..)
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
16 import Data.Map as M (Map, lookup, delete, size, unionWith)
17 import Data.Vector (toList,(!),length, singleton)
18 import Control.Applicative ((<$>), (<*>), empty, pure)
21 import qualified Data.HashMap.Strict as H
22 import Data.Aeson.Types
24 -- Variant of Aeson's `.:?` that interprets a String as a
25 -- single-element list, so it is possible to have either
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
34 String s -> parseJSON $ Array (singleton v)
37 class Mergeable a where
38 merge :: a -> a -> a -- bias towads second arg
40 instance (Ord k, Mergeable a) => Mergeable (Map k a) where
41 merge mx my = M.unionWith merge my mx
43 -- instance Mergeable String where
46 instance Mergeable Value where
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
55 instance Eq a => Mergeable [a] where
56 merge xs ys = L.union xs ys
58 data NmcRRService = NmcRRService
67 instance FromJSON NmcRRService where
69 if length a == 6 then NmcRRService
79 instance Mergeable NmcRRService where
82 data NmcRRI2p = NmcRRI2p
83 { i2pDestination :: String
88 instance FromJSON NmcRRI2p where
89 parseJSON (Object o) = NmcRRI2p
90 <$> o .: "destination"
95 instance Mergeable NmcRRI2p where
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)
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)
113 instance Mergeable NmcRRTls where
116 data NmcRRDs = NmcRRDs
120 , dsHashValue :: String
121 } deriving (Show, Eq)
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)
133 instance Mergeable NmcRRDs where
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)
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'] }
168 isIPv4 x = all isNibble $ splitOn "." x
170 if all isDigit x then (read x :: Int) < 256
172 parseJSON (Object o) = NmcDom
180 <*> o .:? "translate"
188 <*> o .:/ "fingerprint"
191 <*> return Nothing -- domMx not parsed
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
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
225 emptyNmcDom = NmcDom Nothing Nothing Nothing Nothing Nothing Nothing
226 Nothing Nothing Nothing Nothing Nothing Nothing
227 Nothing Nothing Nothing Nothing Nothing Nothing
230 -- | Perform query and return error string or parsed domain object
232 (String -> IO (Either String ByteString)) -- ^ query operation action
234 -> IO (Either String NmcDom) -- ^ error string or domain
235 queryNmcDom queryOp key = do
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
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.
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
253 mbase = mergeSelf base
254 base' = mbase {domImport = Nothing}
256 if depth <= 0 then return $ Left "Nesting of imports is too deep"
257 else case domImport mbase of
258 Nothing -> return $ Right base'
260 sub <- queryNmcDom queryOp key
262 Left e -> return $ Left e
263 Right sub' -> mergeImport queryOp (depth - 1) $ sub' `merge` base'
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
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
279 case M.lookup "" map' of
281 Just sub -> (mergeSelf sub) `merge` base'
282 -- recursion depth limited by the size of the record
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)}
289 needed r = srvName r == serv && srvProto r == proto
291 -- | Presence of some elements require removal of some others
292 normalizeDom :: NmcDom -> NmcDom
293 normalizeDom dom = foldr id dom [ srvNormalizer
294 , translateNormalizer
298 nsNormalizer dom = case domNs dom of
300 Just ns -> emptyNmcDom { domNs = domNs dom, domEmail = domEmail dom }
301 translateNormalizer dom = case domTranslate dom of
303 Just tr -> dom { domMap = Nothing }
304 srvNormalizer dom = dom { domService = Nothing, domMx = makemx }
306 makemx = case domService dom of
308 Just svl -> Just $ map makerec (filter needed svl)
310 needed sr = srvName sr == "smtp"
311 && srvProto sr == "tcp"
313 makerec sr = (show (srvPrio sr)) ++ " " ++ (srvHost sr)
315 -- | Merge imports and Selfs and follow the maps tree to get dom
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
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'
329 Left err -> return base'
331 case domMap base'' of
332 Nothing -> return $ Right emptyNmcDom
334 case M.lookup d map of
335 Nothing -> return $ Right emptyNmcDom
336 Just sub -> descendNmcDom queryOp ds sub
338 -- | Initial NmcDom populated with "import" only, suitable for "descend"
340 String -- ^ domain key (without namespace prefix)
341 -> NmcDom -- ^ resulting seed domain
342 seedNmcDom dn = emptyNmcDom { domImport = Just ("d/" ++ dn)}