diff options
-rw-r--r-- | cokiki.hs | 36 | ||||
-rw-r--r-- | lib/Kiki.hs | 78 | ||||
-rw-r--r-- | lib/ProcessUtils.hs | 2 |
3 files changed, 90 insertions, 26 deletions
@@ -20,6 +20,7 @@ import System.IO | |||
20 | import System.Posix.User | 20 | import System.Posix.User |
21 | import CommandLine | 21 | import CommandLine |
22 | import Data.OpenPGP (SymmetricAlgorithm(Unencrypted)) | 22 | import Data.OpenPGP (SymmetricAlgorithm(Unencrypted)) |
23 | import qualified Hosts | ||
23 | 24 | ||
24 | usage = unlines | 25 | usage = unlines |
25 | [ "cokiki <command> [--chroot=ROOTDIR]" | 26 | [ "cokiki <command> [--chroot=ROOTDIR]" |
@@ -42,17 +43,20 @@ usage = unlines | |||
42 | , "COMMANDS" | 43 | , "COMMANDS" |
43 | , "" | 44 | , "" |
44 | , " ssh-client Modify system ssh configuration to respect" | 45 | , " ssh-client Modify system ssh configuration to respect" |
45 | , " /var/cache/kiki/ssh_known_hosts." | 46 | , " /var/cache/kiki/config/ssh_known_hosts." |
46 | , "" | 47 | , "" |
47 | , " ssh-server Modify system ssh configuration to use the" | 48 | , " ssh-server Modify system ssh configuration to use the" |
48 | , " kiki-managed host key at" | 49 | , " kiki-managed host key at" |
49 | , " /var/cache/kiki/ssh_host_rsa_key." | 50 | , " /var/cache/kiki/config/ssh_host_rsa_key." |
50 | , "" | 51 | , "" |
51 | , " strongswan Modify /etc/ipsec.conf to include settings from" | 52 | , " strongswan Modify /etc/ipsec.conf to include settings from" |
52 | , " /var/cache/kiki/ipsec.conf." | 53 | , " /var/cache/kiki/config/ipsec.conf." |
53 | , "" | 54 | , "" |
54 | , " tor Modify /etc/tor/torrc to configure a tor hidden" | 55 | , " tor Modify /etc/tor/torrc to configure a tor hidden" |
55 | , " service for email (smtp), ssh, and http ports." | 56 | , " service for email (smtp), ssh, and http ports." |
57 | , "" | ||
58 | , " hosts Merge hostnames from /var/cache/kiki/config/hosts" | ||
59 | , " into system file /etc/hosts." | ||
56 | ] | 60 | ] |
57 | 61 | ||
58 | main = do | 62 | main = do |
@@ -63,6 +67,7 @@ main = do | |||
63 | ["ssh-server"] -> Just $ sshServer uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir | 67 | ["ssh-server"] -> Just $ sshServer uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir |
64 | ["strongswan"] -> Just $ strongswan uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir | 68 | ["strongswan"] -> Just $ strongswan uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir |
65 | ["tor"] -> Just $ configureTor uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir | 69 | ["tor"] -> Just $ configureTor uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir |
70 | ["hosts"] -> Just $ configureHosts uid <$> Kiki.ㄧchroot <*> Kiki.ㄧhomedir | ||
66 | _ -> Nothing | 71 | _ -> Nothing |
67 | spec = uncurry fancy Kiki.kikiOptions "" | 72 | spec = uncurry fancy Kiki.kikiOptions "" |
68 | errorQuit msg = do | 73 | errorQuit msg = do |
@@ -78,6 +83,7 @@ main = do | |||
78 | maybeReadFile :: FilePath -> IO (Maybe L.ByteString) | 83 | maybeReadFile :: FilePath -> IO (Maybe L.ByteString) |
79 | maybeReadFile path = do | 84 | maybeReadFile path = do |
80 | doesFileExist path >>= bool (return Nothing) (Just <$> L.readFile path) | 85 | doesFileExist path >>= bool (return Nothing) (Just <$> L.readFile path) |
86 | |||
81 | myWriteFile f bs = do | 87 | myWriteFile f bs = do |
82 | createDirectoryIfMissing True (takeDirectory f) | 88 | createDirectoryIfMissing True (takeDirectory f) |
83 | hPutStrLn stderr $ "Writing "++f | 89 | hPutStrLn stderr $ "Writing "++f |
@@ -133,17 +139,29 @@ sshServer uid root cmn = whenRoot uid root cmn $ do | |||
133 | Kiki.importAndRefresh root cmn Unencrypted | 139 | Kiki.importAndRefresh root cmn Unencrypted |
134 | 140 | ||
135 | strongswan uid root cmn = whenRoot uid root cmn $ do | 141 | strongswan uid root cmn = whenRoot uid root cmn $ do |
142 | -- (1) /etc/ipsec.conf <-- 'include /var/cache/kiki/config/ipsec.conf' | ||
136 | -- Parsing as if ssh config, that's not right, but good enough for now. | 143 | -- Parsing as if ssh config, that's not right, but good enough for now. |
137 | ipsecconf <- parseSshConfig . fromMaybe "" <$> maybeReadFile (root "/etc/ipsec.conf") | 144 | ipsecconf <- parseSshConfig . fromMaybe "" <$> maybeReadFile (root "/etc/ipsec.conf") |
138 | let p:gs = groupBy (\_ d -> not $ sshIsDirective "include" d) $ ["#"]:ipsecconf | 145 | let p:gs = groupBy (\_ d -> not $ sshIsDirective "include" d) $ ["#"]:ipsecconf |
139 | got = filter (\(d:ds) -> elem "/var/cache/kiki/config/ipsec.conf" d) gs | 146 | got = filter (\(d:ds) -> elem "/var/cache/kiki/config/ipsec.conf" d) gs |
140 | case got of | 147 | case got of |
141 | _:_ -> do hPutStrLn stderr "ipsec already configured." | 148 | _:_ -> do hPutStrLn stderr "ipsec.conf already configured." |
142 | [] -> do let ipsecconf' = drop 1 $ ipsecconf ++ [stmt] | 149 | [] -> do let ipsecconf' = drop 1 $ ipsecconf ++ [stmt] |
143 | stmt = ["include", " ", "/var/cache/kiki/config/ipsec.conf"] | 150 | stmt = ["include", " ", "/var/cache/kiki/config/ipsec.conf"] |
144 | hPutStrLn stderr "adding include directive" | 151 | hPutStrLn stderr "adding include directive" |
145 | myWriteFile (root "/etc/ipsec.conf") $ unparseSshConfig ipsecconf' | 152 | myWriteFile (root "/etc/ipsec.conf") $ unparseSshConfig ipsecconf' |
146 | -- etc/ipsec.conf <-- 'include /var/cache/kiki/ipsec.conf' | 153 | |
154 | -- (2) /etc/ipsec.secrets/ <- include /var/cache/kiki/config/ipsec.secrets | ||
155 | -- Parsing as if ssh config, that's not right, but good enough for now. | ||
156 | ipsecconf <- parseSshConfig . fromMaybe "" <$> maybeReadFile (root "/etc/ipsec.secrets") | ||
157 | let p:gs = groupBy (\_ d -> not $ sshIsDirective "include" d) $ ["#"]:ipsecconf | ||
158 | got = filter (\(d:ds) -> elem "/var/cache/kiki/config/ipsec.secrets" d) gs | ||
159 | case got of | ||
160 | _:_ -> do hPutStrLn stderr "ipsec.secrets already configured." | ||
161 | [] -> do let ipsecconf' = drop 1 $ ipsecconf ++ [stmt] | ||
162 | stmt = ["include", " ", "/var/cache/kiki/config/ipsec.secrets"] | ||
163 | hPutStrLn stderr "adding include directive" | ||
164 | myWriteFile (root "/etc/ipsec.secrets") $ unparseSshConfig ipsecconf' | ||
147 | Kiki.importAndRefresh root cmn Unencrypted | 165 | Kiki.importAndRefresh root cmn Unencrypted |
148 | 166 | ||
149 | configureTor uid root cmn = whenRoot uid root cmn $ do | 167 | configureTor uid root cmn = whenRoot uid root cmn $ do |
@@ -201,6 +219,14 @@ configureTor uid root cmn = whenRoot uid root cmn $ do | |||
201 | Kiki.importAndRefresh root cmn Unencrypted | 219 | Kiki.importAndRefresh root cmn Unencrypted |
202 | return () | 220 | return () |
203 | 221 | ||
222 | configureHosts uid root cmn = whenRoot uid root cmn $ do | ||
223 | Kiki.importAndRefresh root cmn Unencrypted | ||
224 | hosts <- Hosts.decode . fromMaybe "" <$> maybeReadFile (root "/etc/hosts") | ||
225 | kikihosts <- Hosts.decode . fromMaybe "" <$> maybeReadFile (root "/var/cache/kiki/config/hosts") | ||
226 | let hosts' = hosts `Hosts.plus` kikihosts | ||
227 | case Hosts.diff hosts hosts' of | ||
228 | [] -> hPutStrLn stderr "kiki hosts are already merged." | ||
229 | _ -> myWriteFile (root "/etc/hosts") $ Hosts.encode hosts' | ||
204 | 230 | ||
205 | parseSshConfig :: L.ByteString -> [[L.ByteString]] | 231 | parseSshConfig :: L.ByteString -> [[L.ByteString]] |
206 | parseSshConfig bs = map tokenize $ L8.lines bs | 232 | parseSshConfig bs = map tokenize $ L8.lines bs |
diff --git a/lib/Kiki.hs b/lib/Kiki.hs index d5b3457..468394f 100644 --- a/lib/Kiki.hs +++ b/lib/Kiki.hs | |||
@@ -19,7 +19,7 @@ import Data.OpenPGP | |||
19 | import Data.OpenPGP.Util | 19 | import Data.OpenPGP.Util |
20 | import Data.Ord | 20 | import Data.Ord |
21 | import System.Directory | 21 | import System.Directory |
22 | import System.FilePath.Posix | 22 | import System.FilePath.Posix as FilePath |
23 | import System.IO | 23 | import System.IO |
24 | import System.IO.Temp | 24 | import System.IO.Temp |
25 | import System.IO.Error | 25 | import System.IO.Error |
@@ -38,6 +38,8 @@ import qualified Data.ByteString.Lazy as L | |||
38 | import qualified Data.ByteString.Lazy.Char8 as Char8 | 38 | import qualified Data.ByteString.Lazy.Char8 as Char8 |
39 | import qualified Data.Map.Strict as Map | 39 | import qualified Data.Map.Strict as Map |
40 | import qualified SSHKey as SSH | 40 | import qualified SSHKey as SSH |
41 | import Network.Socket -- (SockAddr) | ||
42 | import ProcessUtils | ||
41 | 43 | ||
42 | import GnuPGAgent (Query(..)) | 44 | import GnuPGAgent (Query(..)) |
43 | import CommandLine | 45 | import CommandLine |
@@ -323,7 +325,7 @@ refreshCache rt rootdir = do | |||
323 | timeout = -1 -- TODO: set milisecond timeout on dotlock | 325 | timeout = -1 -- TODO: set milisecond timeout on dotlock |
324 | createDirectoryIfMissing True cachedir | 326 | createDirectoryIfMissing True cachedir |
325 | tmpdir <- createTempDirectory cachedir ("transaction." ++ takeBaseName destdir) | 327 | tmpdir <- createTempDirectory cachedir ("transaction." ++ takeBaseName destdir) |
326 | createSymbolicLink tmpdir (tmpdir ++ ".link") | 328 | createSymbolicLink (makeRelative cachedir tmpdir) (tmpdir ++ ".link") |
327 | lock <- dotlock_create destdir 0 | 329 | lock <- dotlock_create destdir 0 |
328 | T.mapM (flip dotlock_take timeout) lock | 330 | T.mapM (flip dotlock_take timeout) lock |
329 | let mkpath pth = tmpdir </> unslash (makeRelative destdir pth) | 331 | let mkpath pth = tmpdir </> unslash (makeRelative destdir pth) |
@@ -336,7 +338,7 @@ refreshCache rt rootdir = do | |||
336 | -- otherwise call readyReadBeforeWrite on them. | 338 | -- otherwise call readyReadBeforeWrite on them. |
337 | rename (tmpdir ++ ".link") destdir | 339 | rename (tmpdir ++ ".link") destdir |
338 | er <- T.mapM dotlock_release lock | 340 | er <- T.mapM dotlock_release lock |
339 | void $ T.mapM removeDirectoryRecursive oldcommit | 341 | void $ T.mapM removeDirectoryRecursive (FilePath.combine cachedir <$> oldcommit) |
340 | -- Present transaction is Write only (or Write-Before-Read) which is fine. | 342 | -- Present transaction is Write only (or Write-Before-Read) which is fine. |
341 | -- If ever Read-Before-Write is required, uncomment and use: | 343 | -- If ever Read-Before-Write is required, uncomment and use: |
342 | -- let readyReadBeforeWrite pth = do | 344 | -- let readyReadBeforeWrite pth = do |
@@ -367,6 +369,10 @@ refreshCache rt rootdir = do | |||
367 | wr f bs | 369 | wr f bs |
368 | write = write' writeFile | 370 | write = write' writeFile |
369 | writeL = write' L.writeFile | 371 | writeL = write' L.writeFile |
372 | writeL077 f bs = do | ||
373 | old_umask <- setFileCreationMask 0o077 | ||
374 | writeL f bs | ||
375 | setFileCreationMask old_umask | ||
370 | 376 | ||
371 | let names = do wk <- rtWorkingKey rt | 377 | let names = do wk <- rtWorkingKey rt |
372 | -- XXX unnecessary signature check | 378 | -- XXX unnecessary signature check |
@@ -436,6 +442,11 @@ refreshCache rt rootdir = do | |||
436 | (mkpath "ipsec.d/private/" ++ Char8.unpack oname++".pem") | 442 | (mkpath "ipsec.d/private/" ++ Char8.unpack oname++".pem") |
437 | "missing ipsec key?" | 443 | "missing ipsec key?" |
438 | 444 | ||
445 | -- TODO: probably we should add multiple entries for the case that there | ||
446 | -- are multiple secret master-keys each with distinct tor and ipsec keys. | ||
447 | writeL077 (mkpath "ipsec.secrets") | ||
448 | $ ": RSA /var/cache/kiki/config/ipsec.d/private/" <> oname <> ".pem" | ||
449 | |||
439 | writeSecret "ssh-client" | 450 | writeSecret "ssh-client" |
440 | (mkpath "root/.ssh/id_rsa") | 451 | (mkpath "root/.ssh/id_rsa") |
441 | "missing ssh-client key?" | 452 | "missing ssh-client key?" |
@@ -457,32 +468,57 @@ refreshCache rt rootdir = do | |||
457 | either warn (write $ mkpath "ipsec.d/certs/" ++ Char8.unpack oname++".pem") | 468 | either warn (write $ mkpath "ipsec.d/certs/" ++ Char8.unpack oname++".pem") |
458 | $ show_pem' "ipsec" grip (rtKeyDB rt) pemFromPacket | 469 | $ show_pem' "ipsec" grip (rtKeyDB rt) pemFromPacket |
459 | 470 | ||
460 | let cs = filter notme (Map.elems $ rtKeyDB rt) | 471 | let onionkeys = mapMaybe namedContact $ Map.elems $ rtKeyDB rt |
472 | cs = filter (\(_,_,kd) -> notme kd) onionkeys | ||
461 | kk = keykey (fromJust $ rtWorkingKey rt) | 473 | kk = keykey (fromJust $ rtWorkingKey rt) |
462 | notme kd = keykey (keyPacket kd) /= kk | 474 | notme kd = keykey (keyPacket kd) /= kk |
463 | 475 | ||
464 | installConctact :: KeyData -> IO Char8.ByteString | 476 | namedContact kd = do |
465 | installConctact kd = do | ||
466 | -- The getHostnames command requires a valid cross-signed tor key | 477 | -- The getHostnames command requires a valid cross-signed tor key |
467 | -- for each onion name returned in (_,(ns,_)). | 478 | -- for each onion name returned in (_,(ns,_)). |
468 | let (addr,(ns,_)) = getHostnames kd | 479 | let (addr,(ns,_)) = getHostnames kd |
469 | contactname = fmap Char8.unpack $ listToMaybe ns -- only first onion name. | 480 | fmap (\n -> (n,addr, kd)) $ listToMaybe ns -- only first onion name. |
470 | flip (maybe $ return Char8.empty) contactname $ \contactname -> do | 481 | |
482 | installConctact :: (L.ByteString, SockAddr, KeyData) -> IO Char8.ByteString | ||
483 | installConctact (contactname,addr,kd) = do | ||
471 | 484 | ||
472 | let cpath = interp (Map.singleton "onion" contactname) "ipsec.d/certs/%(onion).pem" | 485 | let cpath = interp (Map.singleton "onion" (Char8.unpack contactname)) "ipsec.d/certs/%(onion).pem" |
473 | their_master = packet $ keyMappedPacket kd | 486 | their_master = packet $ keyMappedPacket kd |
474 | -- We find all cross-certified ipsec keys for the given cross-certified onion name. | 487 | -- We find all cross-certified ipsec keys for the given cross-certified onion name. |
475 | ipsecs :: [Packet] | 488 | ipsecs :: [Packet] |
476 | ipsecs = sortOn (Down . timestamp) | 489 | ipsecs = sortOn (Down . timestamp) |
477 | $ getSubkeys CrossSigned their_master (keySubKeys kd) "ipsec" | 490 | $ getSubkeys CrossSigned their_master (keySubKeys kd) "ipsec" |
478 | ++ getSubkeys CrossSigned their_master (keySubKeys kd) "strongswan" | 491 | -- ++ getSubkeys CrossSigned their_master (keySubKeys kd) "strongswan" |
492 | sshs :: [Packet] | ||
493 | sshs = sortOn (Down . timestamp) | ||
494 | $ getSubkeys CrossSigned their_master (keySubKeys kd) "ssh-server" | ||
479 | bss <- forM (take 1 ipsecs) $ \k -> do | 495 | bss <- forM (take 1 ipsecs) $ \k -> do |
480 | let warn' x = warn x >> return Char8.empty | 496 | let warn' x = warn x >> return Char8.empty |
481 | flip (either warn') (pemFromPacket k :: Either String String) $ \pem -> do | 497 | flip (either warn') (pemFromPacket k :: Either String String) $ \pem -> do |
482 | write (mkpath cpath) pem | 498 | write (mkpath cpath) pem |
483 | return $ strongswanForContact addr contactname | 499 | case take 1 sshs of |
500 | [sshkey] -> do | ||
501 | (_,(sout,serr)) <- runExternal "(f=$(mktemp); cat > \"$f\"; ssh-keygen -l -f \"$f\" | (read _ hash _; echo -n $hash.ssh.cryptonomic.net) | tr -d ':')" | ||
502 | (Just $ sshblobFromPacket sshkey) | ||
503 | -- ssh-keygen -l -f /dev/stdin | ||
504 | -- putStrLn $ "wtf="++show(sout,serr, sshblobFromPacket sshkey) | ||
505 | return $ strongswanForContact addr contactname (Char8.fromChunks [sout]) | ||
506 | [] -> error "fuck." | ||
484 | return $ Char8.concat bss | 507 | return $ Char8.concat bss |
485 | 508 | ||
509 | known_hosts = L.concat $ map getssh onionkeys | ||
510 | |||
511 | getssh (contactname,addr,kd) = do | ||
512 | let their_master = packet $ keyMappedPacket kd | ||
513 | sshs :: [Packet] | ||
514 | sshs = sortOn (Down . timestamp) | ||
515 | $ getSubkeys CrossSigned their_master (keySubKeys kd) "ssh-server" | ||
516 | blobs = mapMaybe sshblobFromPacketL sshs | ||
517 | taggedblobs = map (\b -> contactname <> " " <> b) blobs | ||
518 | Char8.unlines taggedblobs | ||
519 | |||
520 | writeL (mkpath "ssh_known_hosts") known_hosts | ||
521 | |||
486 | cons <- mapM installConctact cs | 522 | cons <- mapM installConctact cs |
487 | writeL (mkpath "ipsec.conf") . Char8.unlines | 523 | writeL (mkpath "ipsec.conf") . Char8.unlines |
488 | $ [ "conn %default" | 524 | $ [ "conn %default" |
@@ -497,7 +533,7 @@ refreshCache rt rootdir = do | |||
497 | , " leftsubnet=" <> Char8.pack (showA wkaddr) <> "/128" | 533 | , " leftsubnet=" <> Char8.pack (showA wkaddr) <> "/128" |
498 | , " leftauth=pubkey" | 534 | , " leftauth=pubkey" |
499 | , " leftid=" <> Char8.pack (showA wkaddr) | 535 | , " leftid=" <> Char8.pack (showA wkaddr) |
500 | , " leftrsasigkey=" <> oname | 536 | , " leftrsasigkey=" <> oname <> ".pem" |
501 | , " leftikeport=4500" | 537 | , " leftikeport=4500" |
502 | , " rightikeport=4500" | 538 | , " rightikeport=4500" |
503 | , " right=%any" | 539 | , " right=%any" |
@@ -508,13 +544,13 @@ refreshCache rt rootdir = do | |||
508 | ] ++ filter (not . Char8.null) cons | 544 | ] ++ filter (not . Char8.null) cons |
509 | commit | 545 | commit |
510 | 546 | ||
511 | strongswanForContact addr oname = Char8.unlines | 547 | strongswanForContact addr oname rightip = Char8.unlines |
512 | [ "conn " <> p oname | 548 | [ "conn " <> oname |
513 | , " right=%" <> p oname <> ".ipv4" | 549 | , " right=lan." <> rightip |
514 | , " rightsubnet=" <> p (showA addr) <> "/128" | 550 | , " rightsubnet=" <> p (showA addr) <> "/128" |
515 | , " rightauth=pubkey" | 551 | , " rightauth=pubkey" |
516 | , " rightid=" <> p (showA addr) | 552 | , " rightid=" <> p (showA addr) |
517 | , " rightrsasigkey=" <> p (oname) <> ".pem" | 553 | , " rightrsasigkey=" <> oname <> ".pem" |
518 | ] | 554 | ] |
519 | where p = Char8.pack | 555 | where p = Char8.pack |
520 | 556 | ||
@@ -578,11 +614,11 @@ interp vars raw = es >>= interp1 | |||
578 | where (key,rest) = break (==')') str | 614 | where (key,rest) = break (==')') str |
579 | interp1 plain = plain | 615 | interp1 plain = plain |
580 | 616 | ||
581 | sshblobFromPacket k = blob | 617 | sshblobFromPacket k = Char8.unpack $ fromJust $ sshblobFromPacketL k |
582 | where | 618 | |
583 | Just (RSAKey (MPI n) (MPI e)) = rsaKeyFromPacket k | 619 | sshblobFromPacketL k = do |
584 | bs = SSH.keyblob (n,e) | 620 | RSAKey (MPI n) (MPI e) <- rsaKeyFromPacket k |
585 | blob = Char8.unpack bs | 621 | return $ SSH.keyblob (n,e) |
586 | 622 | ||
587 | {- | 623 | {- |
588 | replaceSshServerKeys root cmn = do | 624 | replaceSshServerKeys root cmn = do |
diff --git a/lib/ProcessUtils.hs b/lib/ProcessUtils.hs index b89edb9..1a9cc04 100644 --- a/lib/ProcessUtils.hs +++ b/lib/ProcessUtils.hs | |||
@@ -126,10 +126,12 @@ readProcessWithErrorH cmd args stdin erH = do | |||
126 | runExternal :: String -> Maybe String -> IO (Int,(S.ByteString,S.ByteString)) | 126 | runExternal :: String -> Maybe String -> IO (Int,(S.ByteString,S.ByteString)) |
127 | runExternal cmd input = do | 127 | runExternal cmd input = do |
128 | cwd <- getCurrentDirectory | 128 | cwd <- getCurrentDirectory |
129 | {- | ||
129 | putStr $ takeFileName cwd ++ "> " ++ cmd | 130 | putStr $ takeFileName cwd ++ "> " ++ cmd |
130 | ++ case input of | 131 | ++ case input of |
131 | Nothing -> "\n" | 132 | Nothing -> "\n" |
132 | Just s -> " <<EOF\n" ++ s ++ "EOF\n" | 133 | Just s -> " <<EOF\n" ++ s ++ "EOF\n" |
134 | -} | ||
133 | let p = (shell cmd) { std_in = maybe Inherit (const CreatePipe) input | 135 | let p = (shell cmd) { std_in = maybe Inherit (const CreatePipe) input |
134 | , std_out = CreatePipe | 136 | , std_out = CreatePipe |
135 | , std_err = CreatePipe } | 137 | , std_err = CreatePipe } |