packages feed

aws-ec2-knownhosts (empty) → 0.1.0.0

raw patch · 8 files changed

+463/−0 lines, 8 filesdep +aesondep +asyncdep +attoparsec

Dependencies added: aeson, async, attoparsec, aws-ec2-knownhosts, base, bytestring, directory, filepath, io-streams, process, system-filepath, text, text-format, turtle

Files

+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2015-present, Bitnomial, Inc.+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+  contributors may be used to endorse or promote products derived from+  this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ aws-ec2-knownhosts.cabal view
@@ -0,0 +1,76 @@+name:                aws-ec2-knownhosts+version:             0.1.0.0+synopsis:            Capture and manage AWS EC2 known_host pubkeys+description:         Capture and manage AWS EC2 known_host pubkeys via the AWS API command line tool.+license:             BSD3+license-file:        LICENSE+author:              Luke Hoersten+maintainer:          luke@bitnomial.com, opensource@bitnomial.com+copyright:           Bitnomial, Inc. (c) 2015+category:            AWS, Network+build-type:          Simple+cabal-version:       >=1.10+homepage:            http://github.com/bitnomial/aws-ec2-knownhosts+bug-reports:         http://github.com/bitnomial/aws-ec2-knownhosts/issues+++library+  hs-source-dirs:      src+  default-language:    Haskell2010++  exposed-modules: AWS.KnownHosts+                 , AWS.PubKeys+                 , AWS.Types++  build-depends: base        >= 4.7   && < 4.11+               , aeson       >= 1.0   && < 1.3+               , async       >= 2.0   && < 2.2+               , attoparsec  >= 0.12  && < 0.14+               , bytestring  >= 0.10  && < 0.11+               , directory   >= 1.2   && < 1.4+               , filepath    >= 1.4   && < 1.5+               , io-streams  >= 1.3.4 && < 1.5+               , process     >= 1.4   && < 1.7+               , text        >= 1.1   && < 1.3+               , text-format >= 0.3   && < 0.4+++executable aws-ec2-pubkeys+  main-is:           Main.hs+  hs-source-dirs:    exec/aws-ec2-pubkeys/+  default-language:  Haskell2010+  ghc-options:       -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -rtsopts++  build-depends: base               >= 4.7 && < 4.11+               , aws-ec2-knownhosts+               , io-streams         >= 1.1 && < 1.5+               , system-filepath    >= 0.4 && < 0.5+               , turtle             >= 1.3 && < 1.5+++executable aws-ec2-knownhosts+  main-is:           Main.hs+  hs-source-dirs:    exec/aws-ec2-knownhosts/+  default-language:  Haskell2010+  ghc-options:       -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -rtsopts++  build-depends: base               >= 4.7 && < 4.11+               , aeson              >= 1.0 && < 1.3+               , aws-ec2-knownhosts+               , io-streams         >= 1.1 && < 1.5+               , system-filepath    >= 0.4 && < 0.5+               , turtle             >= 1.3 && < 1.5+++executable aws-ec2-keysync+  main-is:           Main.hs+  hs-source-dirs:    exec/aws-ec2-keysync/+  default-language:  Haskell2010+  ghc-options:       -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -rtsopts++  build-depends: base               >= 4.7 && < 4.11+               , aeson              >= 1.0 && < 1.3+               , aws-ec2-knownhosts+               , io-streams         >= 1.1 && < 1.5+               , system-filepath    >= 0.4 && < 0.5+               , turtle             >= 1.3 && < 1.5
+ exec/aws-ec2-keysync/Main.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import           Control.Monad                ((<=<))+import           Data.Aeson                   (Result (..), fromJSON, json')+import           Data.Bool                    (bool)+import           Data.Function                (on)+import           Data.List                    (foldl')+import           Data.Monoid                  ((<>))+import           Filesystem.Path.CurrentOS    (encodeString, fromText)+import           Prelude                      hiding (FilePath)+import qualified System.IO.Streams            as Streams+import           System.IO.Streams.Attoparsec (parseFromStream)+import           Turtle                       (ExitCode (..), FilePath, Parser, Text, argPath, argText, exit, format,+                                               fp, isRegularFile, options, proc, rm, stat)++import           AWS.PubKeys                  (writePubKeys)+import           AWS.Types                    (Ec2Instance (..))+++main :: IO ()+main = options "Synchronize pubkeys from EC2 to AWS S3" args >>= processPubKeys+++args :: Parser (FilePath, FilePath, Text)+args = (,,)+    <$> argPath "local_pubkey_filename" "Local pubkey file to send to S3"+    <*> argPath "remote_pubkey_filename" "Destination pubkey file to send to S3"+    <*> argText "s3_bucket_name" "AWS S3 bucket name to store pubkeys"+++processPubKeys :: (FilePath, FilePath, Text) -> IO ()+processPubKeys (localPubKeyFile, remotePubKeyFile, bucketName) = do+    newPubKeys <- readPubKeys localPubKeyFile+    remotePubKeys <- getFromS3 remotePubKeyFile tempRemoteFile bucketName+    writePubKeys (encodeString mergedKeyFile) $ merge remotePubKeys newPubKeys+    copyToS3 mergedKeyFile remotePubKeyFile bucketName+  where+    tempRemoteFile :: FilePath+    tempRemoteFile = "/tmp/" <> fromText bucketName <> "-" <> remotePubKeyFile+    mergedKeyFile = "/tmp/merged-keys.json"+++readPubKeys :: FilePath -> IO [Ec2Instance]+readPubKeys pubKeyFile =+    Streams.withFileAsInput (encodeString pubKeyFile) (handleResult <=< parseFromStream pubKeyParser)+  where pubKeyParser = fmap fromJSON json'+++merge :: [Ec2Instance] -> [Ec2Instance] -> [Ec2Instance]+merge = foldl' replace+  where+    replace instancePubKeys newInstancePubKey =+        newInstancePubKey : filter (compareFqdn newInstancePubKey) instancePubKeys+++compareFqdn :: Ec2Instance -> Ec2Instance -> Bool+compareFqdn = (/=) `on` fqdn+++handleResult :: Result a -> IO a+handleResult (Success a) = return a+handleResult (Error s)   = putStrLn ("Parse error: " <> s) >> exit (ExitFailure (-1))+++getFromS3 :: FilePath -> FilePath -> Text -> IO [Ec2Instance]+getFromS3 remoteFile tempRemoteFile bucketName = do+    _ <- proc "aws" [ "s3", "cp" , source, format fp tempRemoteFile ] ""+    isRegularFile <$> stat tempRemoteFile >>= bool (return []) (readPubKeys tempRemoteFile)+  where+    source = "s3://" <> bucketName <> "/" <> format fp remoteFile+++copyToS3 :: FilePath -> FilePath -> Text -> IO ()+copyToS3 localFile remoteFile bucketName =+    proc "aws" ["s3", "cp", format fp localFile, dest] "" >>= checkUpload+  where+    dest = "s3://" <> bucketName <> "/" <> format fp remoteFile+    checkUpload ExitSuccess = putStrLn "Upload successful"+    checkUpload _ = putStrLn "Failure uploading pubkey file. Check AWS credentials." >> exit (ExitFailure (-1))+++cleanup :: FilePath -> FilePath -> IO ()+cleanup tempRemoteFile mergedKeyFile = rm tempRemoteFile >> rm mergedKeyFile
+ exec/aws-ec2-knownhosts/Main.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import           Data.Aeson                   (Result (..), fromJSON, json)+import           Data.Maybe                   (maybe)+import           Filesystem.Path.CurrentOS    (encodeString)+import           Prelude                      hiding (FilePath)+import qualified System.IO.Streams            as Streams+import           System.IO.Streams.Attoparsec (parseFromStream)+import           Turtle                       (FilePath, Parser, argPath, options)++import           AWS.KnownHosts               (updateKnownHosts)+import           AWS.Types                    (Ec2Instance (..))+++main :: IO ()+main = options "Update known_hosts file with pubkeys from EC2" args >>= readKeys >>= maybe noKeys updateKnownHosts+++args :: Parser FilePath+args = argPath "pubkey_file" "Path to JSON pubkey file to update knownhosts"+++readKeys :: FilePath -> IO (Maybe [Ec2Instance])+readKeys = fmap (maybeResult . fromJSON) . flip Streams.withFileAsInput (parseFromStream json) . encodeString+++maybeResult :: Result a -> Maybe a+maybeResult (Success x) = Just x+maybeResult (Error _)   = Nothing+++noKeys :: IO ()+noKeys = putStrLn "No keys were found in that pub_key_file"
+ exec/aws-ec2-pubkeys/Main.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import           Filesystem.Path.CurrentOS    (encodeString)+import qualified System.IO.Streams            as Streams+import           System.IO.Streams.Attoparsec (parseFromStream)+import           Turtle                       (Parser, argPath, options)++import           AWS.PubKeys                  (getPubKeys, instanceParser, writePubKeys)+import           AWS.Types                    (Ec2Instance (..))+++main :: IO ()+main = options "Get SSH pubkeys from EC2" args >>= processPubKeys+++args :: Parser (FilePath, FilePath)+args = (,)+    <$> (encodeString <$> argPath "instance_file" "File with information about EC2 hosts")+    <*> (encodeString <$> argPath "pubkey_file" "Path to JSON pubkey file to output")+++-- TODO Implement a function to poll until all pubkeys are available+processPubKeys :: (FilePath, FilePath) -> IO ()+processPubKeys (instanceFile, pubKeyFile) = writePubKeys pubKeyFile =<< getPubKeys =<< readInstances instanceFile+++readInstances :: FilePath -> IO [Ec2Instance]+readInstances = ($ parseFromStream instanceParser) . Streams.withFileAsInput
+ src/AWS/KnownHosts.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections     #-}++module AWS.KnownHosts+  ( updateKnownHosts+  ) where++import           Data.ByteString.Char8     (ByteString)+import           Data.Monoid               ((<>))+import           Data.Text.Encoding        (encodeUtf8)+import           Data.Text.Format          (Only (..), format)+import qualified Data.Text.Lazy            as LT+import           GHC.IO.Handle             (BufferMode (NoBuffering))+import           Prelude                   hiding (filter, takeWhile)+import           System.Directory          (getHomeDirectory)+import           System.Exit               (ExitCode)+import           System.FilePath.Posix     ((</>))+import           System.IO                 (IOMode (AppendMode))+import qualified System.IO.Streams         as Streams+import           System.IO.Streams.File    (withFileAsOutputExt)+import           System.IO.Streams.Process (runInteractiveCommand)+import           System.Process            (waitForProcess)++import           AWS.Types                 (Ec2Instance (..), Key (..))+++updateKnownHosts :: [Ec2Instance] -> IO ()+updateKnownHosts ks = do+    homeDir <- getHomeDirectory+    withFileAsOutputExt (knownHosts homeDir) AppendMode NoBuffering $ updateKeys ks+  where+    knownHosts homeDir = homeDir </> ".ssh" </> "known_hosts"+++updateKeys :: [Ec2Instance] -> Streams.OutputStream ByteString -> IO ()+updateKeys ks out = do+    out' <- Streams.intersperse "\n" out+    mapM_ (updateKey out') ks+    Streams.write (Just "\n") out+++updateKey :: Streams.OutputStream ByteString -> Ec2Instance -> IO ()+updateKey out inst | Just k <- instancePubKey inst =+    removeKey inst `seq`+    Streams.write (Just $ encodeUtf8 (fqdn inst <> "," <> dns inst <> " " <> keyType k <> " " <> pubKey k)) out+updateKey _ _ = return ()+++removeKey :: Ec2Instance -> IO ExitCode+removeKey inst = do+    (_,_,_,pid1) <- runInteractiveCommand . LT.unpack . format removeKeyCommand . Only $ dns inst+    waitForProcess pid1+    (_,_,_,pid2) <- runInteractiveCommand . LT.unpack . format removeKeyCommand . Only $ fqdn inst+    waitForProcess pid2+  where+    removeKeyCommand = "ssh-keygen -R {}"
+ src/AWS/PubKeys.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections     #-}++module AWS.PubKeys+  ( getPubKeys+  , writePubKeys+  , instanceParser+  ) where++import           Control.Applicative              (Alternative, (*>), (<*), (<|>))+import           Control.Concurrent.Async         (mapConcurrently)+import           Data.Aeson                       (decodeStrict', encode)+import qualified Data.Attoparsec.ByteString       as W8+import           Data.Attoparsec.ByteString.Char8 (Parser, endOfInput, endOfLine, isEndOfLine, isSpace, manyTill',+                                                   skipSpace, string, (<?>))+import qualified Data.Attoparsec.ByteString.Char8 as C8+import           Data.ByteString.Char8            (ByteString, filter)+import           Data.ByteString.Lazy.Char8       (toStrict)+import           Data.List                        (find)+import           Data.Maybe                       (catMaybes, mapMaybe)+import           Data.Text.Encoding               (decodeUtf8)+import           Data.Text.Format                 (format)+import qualified Data.Text.Lazy                   as LT+import           Prelude                          hiding (filter, takeWhile)+import qualified System.IO.Streams                as Streams+import           System.IO.Streams.Attoparsec     (parseFromStream)+import           System.IO.Streams.Process        (runInteractiveCommand)++import           AWS.Types                        (Ec2Instance (..), Key (..))+++getPubKeys :: [Ec2Instance] -> IO [Ec2Instance]+getPubKeys = fmap catMaybes <$> mapConcurrently getPubKey+++getPubKey :: Ec2Instance -> IO (Maybe Ec2Instance)+getPubKey inst = do+    (_, stdout, _, _) <- runInteractiveCommand command+    keys' <- Streams.map (filter (/= '\r')) stdout >>= parseFromStream keyParser+    return $ (\k -> inst{instancePubKey=Just k}) <$> find ((keytype' ==) . keyType) keys'+  where+    keytype' = "ecdsa-sha2-nistp256"+    command = LT.unpack $ format consoleOutput (region inst, instanceId inst)+    consoleOutput = "aws ec2 get-console-output --region {} --output text --instance-id {}"+++writePubKeys :: FilePath -> [Ec2Instance] -> IO ()+writePubKeys file = Streams.withFileAsOutput file . Streams.write . Just . toStrict . encode+++instanceParser :: Parser [Ec2Instance]+instanceParser = mapMaybe decodeStrict' <$> (W8.takeTill isEndOfLine `W8.sepBy` endOfLine <* endOfInput) <?> "instances"+++keyParser :: Parser [Key]+keyParser = skipToKeys *> keys <?> "keyParser"+++skipToKeys :: Parser ()+skipToKeys = skipLine `skipTill` keysBegin <?> "skipToKeys"+  where+    skipTill :: Alternative f => f a -> f b -> f b+    skipTill s n = n <|> s *> skipTill s n+++skipLine :: Parser ()+skipLine = W8.skipWhile (not . isEndOfLine) <* endOfLine <?> "skip line"+++line :: ByteString -> Parser ()+line s = string s *> endOfLine <?> "exact line"+++keysBegin :: Parser ()+keysBegin = line "-----BEGIN SSH HOST KEY KEYS-----" <?> "keys begin"+++keysEnd :: Parser ()+keysEnd = line "-----END SSH HOST KEY KEYS-----" <?> "keys end"+++keys :: Parser [Key]+keys = manyTill' key keysEnd <?> "keys"+++key :: Parser Key+key = do+    keyType' <- decodeUtf8 <$> C8.takeTill isSpace+    skipSpace+    pubKey' <- decodeUtf8 <$> C8.takeTill isSpace+    skipLine+    return $ Key keyType' pubKey'+    <?> "key"
+ src/AWS/Types.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}++module AWS.Types where++import           Data.Aeson       (FromJSON (..), ToJSON (..), Value (..), object, (.:), (.:?), (.=))+import           Data.Aeson.Types (typeMismatch)+import           Data.Text        (Text)+++data Key = Key+    { keyType :: Text+    , pubKey  :: Text+    } deriving (Show, Eq)+++data Ec2Instance = Ec2Instance+    { fqdn           :: Text+    , region         :: Text+    , instanceId     :: Text+    , dns            :: Text+    , initKey        :: Text+    , instancePubKey :: Maybe Key+    } deriving (Show, Eq)+++instance FromJSON Key where+    parseJSON (Object v) = Key+        <$> v .: "type"+        <*> v .: "pubkey"+    parseJSON invalid = typeMismatch "Key" invalid+++instance ToJSON Key where+    toJSON x = object+        [ "type"        .= keyType x+        , "pubkey"      .= pubKey x+        ]+++instance FromJSON Ec2Instance where+    parseJSON (Object v) = Ec2Instance+        <$> v .: "fqdn"+        <*> v .: "region"+        <*> v .: "instance_id"+        <*> v .: "dns"+        <*> v .: "init_key"+        <*> v .:? "pubkey"+    parseJSON invalid = typeMismatch "Ec2Instance" invalid+++instance ToJSON Ec2Instance where+    toJSON x = object+        [ "fqdn"        .= fqdn x+        , "region"      .= region x+        , "instance_id" .= instanceId x+        , "dns"         .= dns x+        , "init_key"    .= initKey x+        , "pubkey"      .= instancePubKey x+        ]