-
Notifications
You must be signed in to change notification settings - Fork 701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement DNS-based mirror bootstrap protocol #3947
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
module Distribution.Client.Security.DNS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
( queryBootstrapMirrors | ||
) where | ||
|
||
import Prelude () | ||
import Distribution.Client.Compat.Prelude | ||
|
||
import Control.Monad | ||
import Control.DeepSeq (force) | ||
import Control.Exception (SomeException, evaluate, try) | ||
import Network.URI (URI(..), URIAuth(..), parseURI) | ||
|
||
import Distribution.Simple.Utils | ||
import Distribution.Verbosity | ||
import Distribution.Simple.Program.Db | ||
( emptyProgramDb, addKnownProgram | ||
, configureAllKnownPrograms, lookupProgram ) | ||
import Distribution.Simple.Program | ||
( simpleProgram | ||
, programInvocation | ||
, getProgramInvocationOutput ) | ||
import Distribution.Compat.Exception (displayException) | ||
|
||
-- | Try to lookup RFC1464-encoded mirror urls for a Hackage | ||
-- repository url by performing a DNS TXT lookup on the | ||
-- @_mirrors.@-prefixed URL hostname. | ||
-- | ||
-- Example: for @http://hackage.haskell.org/@ | ||
-- perform a DNS TXT query for the hostname | ||
-- @_mirrors.hackage.haskell.org@ which may look like e.g. | ||
-- | ||
-- > _mirrors.hackage.haskell.org. 300 IN TXT | ||
-- > "0.urlbase=http://hackage.fpcomplete.com/" | ||
-- > "1.urlbase=http://objects-us-west-1.dream.io/hackage-mirror/" | ||
-- | ||
-- NB: hackage-security doesn't require DNS lookups being trustworthy, | ||
-- as the trust is established via the cryptographically signed TUF | ||
-- meta-data that is retrieved from the resolved Hackage repository. | ||
-- Moreover, we already have to protect against a compromised | ||
-- @hackage.haskell.org@ DNS entry, so an the additional | ||
-- @_mirrors.hackage.haskell.org@ DNS entry in the same SOA doesn't | ||
-- constitute a significant new attack vector anyway. | ||
-- | ||
queryBootstrapMirrors :: Verbosity -> URI -> IO [URI] | ||
queryBootstrapMirrors verbosity repoUri | ||
| Just auth <- uriAuthority repoUri = do | ||
progdb <- configureAllKnownPrograms verbosity $ | ||
addKnownProgram nslookupProg emptyProgramDb | ||
|
||
case lookupProgram nslookupProg progdb of | ||
Nothing -> do | ||
warn verbosity "'nslookup' tool missing - can't locate mirrors" | ||
return [] | ||
|
||
Just nslookup -> do | ||
let mirrorsDnsName = "_mirrors." ++ uriRegName auth | ||
|
||
mirrors' <- try $ do | ||
out <- getProgramInvocationOutput verbosity $ | ||
programInvocation nslookup ["-query=TXT", mirrorsDnsName] | ||
evaluate (force $ extractMirrors mirrorsDnsName out) | ||
|
||
mirrors <- case mirrors' of | ||
Left e -> do | ||
warn verbosity ("Caught exception during _mirrors lookup:"++ | ||
displayException (e :: SomeException)) | ||
return [] | ||
Right v -> return v | ||
|
||
if null mirrors | ||
then warn verbosity ("No mirrors found for " ++ show repoUri) | ||
else do info verbosity ("located " ++ show (length mirrors) ++ | ||
" mirrors for " ++ show repoUri ++ " :") | ||
forM_ mirrors $ \url -> info verbosity ("- " ++ show url) | ||
|
||
return mirrors | ||
|
||
| otherwise = return [] | ||
where | ||
nslookupProg = simpleProgram "nslookup" | ||
|
||
-- | Extract list of mirrors from @nslookup -query=TXT@ output. | ||
extractMirrors :: String -> String -> [URI] | ||
extractMirrors hostname s0 = mapMaybe (parseURI . snd) . sort $ vals | ||
where | ||
vals = [ (kn,v) | (h,ents) <- fromMaybe [] $ parseNsLookupTxt s0 | ||
, h == hostname | ||
, e <- ents | ||
, Just (k,v) <- [splitRfc1464 e] | ||
, Just kn <- [isUrlBase k] | ||
] | ||
|
||
isUrlBase :: String -> Maybe Int | ||
isUrlBase s | ||
| isSuffixOf ".urlbase" s, not (null ns), all isDigit ns = readMaybe ns | ||
| otherwise = Nothing | ||
where | ||
ns = take (length s - 8) s | ||
|
||
-- | Parse output of @nslookup -query=TXT $HOSTNAME@ tolerantly | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you really sure you don't want to use a parser combinator for this? ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, what's the benefit? :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...well, ideally you would have written it with parser combinators to begin with :P |
||
parseNsLookupTxt :: String -> Maybe [(String,[String])] | ||
parseNsLookupTxt = go0 [] [] | ||
where | ||
-- approximate grammar: | ||
-- <entries> := { <entry> } | ||
-- (<entry> starts at begin of line, but may span multiple lines) | ||
-- <entry> := ^ <hostname> TAB "text =" { <qstring> } | ||
-- <qstring> := string enclosed by '"'s ('\' and '"' are \-escaped) | ||
|
||
-- scan for ^ <word> <TAB> "text =" | ||
go0 [] _ [] = Nothing | ||
go0 res _ [] = Just (reverse res) | ||
go0 res _ ('\n':xs) = go0 res [] xs | ||
go0 res lw ('\t':'t':'e':'x':'t':' ':'=':xs) = go1 res (reverse lw) [] (dropWhile isSpace xs) | ||
go0 res lw (x:xs) = go0 res (x:lw) xs | ||
|
||
-- collect at least one <qstring> | ||
go1 res lw qs ('"':xs) = case qstr "" xs of | ||
Just (s, xs') -> go1 res lw (s:qs) (dropWhile isSpace xs') | ||
Nothing -> Nothing -- bad quoting | ||
go1 _ _ [] _ = Nothing -- missing qstring | ||
go1 res lw qs xs = go0 ((lw,reverse qs):res) [] xs | ||
|
||
qstr _ ('\n':_) = Nothing -- We don't support unquoted LFs | ||
qstr acc ('\\':'\\':cs) = qstr ('\\':acc) cs | ||
qstr acc ('\\':'"':cs) = qstr ('"':acc) cs | ||
qstr acc ('"':cs) = Just (reverse acc, cs) | ||
qstr acc (c:cs) = qstr (c:acc) cs | ||
qstr _ [] = Nothing | ||
|
||
-- | Split a TXT string into key and value according to RFC1464. | ||
-- Returns 'Nothing' if parsing fails. | ||
splitRfc1464 :: String -> Maybe (String,String) | ||
splitRfc1464 = go "" | ||
where | ||
go _ [] = Nothing | ||
go acc ('`':c:cs) = go (c:acc) cs | ||
go acc ('=':cs) = go2 (reverse acc) "" cs | ||
go acc (c:cs) | ||
| isSpace c = go acc cs | ||
| otherwise = go (c:acc) cs | ||
|
||
go2 k acc [] = Just (k,reverse acc) | ||
go2 _ _ ['`'] = Nothing | ||
go2 k acc ('`':c:cs) = go2 k (c:acc) cs | ||
go2 k acc (c:cs) = go2 k (c:acc) cs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems worth mentioning (in info) that we're querying DNS to get some mirrors. And it'll only happen once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered that, but I didn't want to have to duplicate the detection in
queryBootstrapMirrors
for when a DNS query is going to occur at all...So what about:
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is what I had in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done