Skip to content

Commit c5f777c

Browse files
committed
downloadURI checks sha256 if it is present in uri fragment.
We can write URIs with a fragment like https://hackage.haskell.org/package/cabal-fmt-0.1.2/cabal-fmt-0.1.2.tar.gz#sha256=aae556efbcaddfd65c6a1c1811b122b0d8c8d00624c8c2e36aabb5e9f9ea9840 and downloadURI will check the hash after download before continuing. The hash check supersedes ETag
1 parent f4ecbf1 commit c5f777c

File tree

1 file changed

+86
-24
lines changed

1 file changed

+86
-24
lines changed

cabal-install/Distribution/Client/HttpUtils.hs

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import Control.DeepSeq
3434
( force )
3535
import Control.Monad
3636
( guard )
37-
import qualified Data.ByteString.Lazy.Char8 as BS
3837
import qualified Paths_cabal_install (version)
3938
import Distribution.Verbosity (Verbosity)
4039
import Distribution.Pretty (prettyShow)
@@ -57,6 +56,7 @@ import System.IO
5756
( withFile, IOMode(ReadMode), hGetContents, hClose )
5857
import System.IO.Error
5958
( isDoesNotExistError )
59+
import Distribution.Parsec (explicitEitherParsec)
6060
import Distribution.Simple.Program
6161
( Program, simpleProgram, ConfiguredProgram, programPath
6262
, ProgramInvocation(..), programInvocation
@@ -74,6 +74,13 @@ import System.Random (randomRIO)
7474
import System.Exit (ExitCode(..))
7575
import Data.Version (showVersion)
7676

77+
import qualified Crypto.Hash.SHA256 as SHA256
78+
import qualified Data.ByteString.Base16 as Base16
79+
import qualified Distribution.Compat.CharParsing as P
80+
import qualified Data.ByteString as BS
81+
import qualified Data.ByteString.Char8 as BS8
82+
import qualified Data.ByteString.Lazy as LBS
83+
import qualified Data.ByteString.Lazy.Char8 as LBS8
7784

7885
------------------------------------------------------------------------------
7986
-- Downloading a URI, given an HttpTransport
@@ -83,6 +90,12 @@ data DownloadResult = FileAlreadyInCache
8390
| FileDownloaded FilePath
8491
deriving (Eq)
8592

93+
data DownloadCheck
94+
= Downloaded -- ^ already downloaded and sha256 matches
95+
| CheckETag String -- ^ already downloaded and we have etag
96+
| NeedsDownload (Maybe BS.ByteString) -- ^ needs download with optional hash check
97+
deriving Eq
98+
8699
downloadURI :: HttpTransport
87100
-> Verbosity
88101
-> URI -- ^ What to download
@@ -96,13 +109,34 @@ downloadURI _transport verbosity uri path | uriScheme uri == "file:" = do
96109

97110
downloadURI transport verbosity uri path = do
98111

99-
let etagPath = path <.> "etag"
100-
targetExists <- doesFileExist path
101-
etagPathExists <- doesFileExist etagPath
102-
-- In rare cases the target file doesn't exist, but the etag does.
103-
etag <- if targetExists && etagPathExists
104-
then Just <$> readFile etagPath
105-
else return Nothing
112+
targetExists <- doesFileExist path
113+
114+
downloadCheck <-
115+
-- if we have uriFrag, then we expect there to be #sha256=...
116+
if not (null uriFrag)
117+
then case sha256parsed of
118+
-- we know the hash, and target exists
119+
Right expected | targetExists -> do
120+
contents <- LBS.readFile path
121+
let actual = SHA256.hashlazy contents
122+
if expected == actual
123+
then return Downloaded
124+
else return (NeedsDownload (Just expected))
125+
126+
-- we known the hash, target doesn't exist
127+
Right expected -> return (NeedsDownload (Just expected))
128+
129+
-- we failed to parse uriFragment
130+
Left err -> die' verbosity $
131+
"Cannot parse URI fragment " ++ uriFrag ++ " " ++ err
132+
133+
-- if there are no uri fragment, use ETag
134+
else do
135+
etagPathExists <- doesFileExist etagPath
136+
-- In rare cases the target file doesn't exist, but the etag does.
137+
if targetExists && etagPathExists
138+
then return (CheckETag etagPath)
139+
else return (NeedsDownload Nothing)
106140

107141
-- Only use the external http transports if we actually have to
108142
-- (or have been told to do so)
@@ -114,12 +148,29 @@ downloadURI transport verbosity uri path = do
114148
| otherwise
115149
= transport
116150

117-
withTempFileName (takeDirectory path) (takeFileName path) $ \tmpFile -> do
151+
case downloadCheck of
152+
Downloaded -> return FileAlreadyInCache
153+
CheckETag etag -> makeDownload transport' Nothing (Just etag)
154+
NeedsDownload hash -> makeDownload transport' hash Nothing
155+
156+
where
157+
makeDownload transport' sha256 etag = withTempFileName (takeDirectory path) (takeFileName path) $ \tmpFile -> do
118158
result <- getHttp transport' verbosity uri etag tmpFile []
119159

120160
-- Only write the etag if we get a 200 response code.
121161
-- A 304 still sends us an etag header.
122162
case result of
163+
-- if we have hash, we don't care about etag.
164+
(200, _) | Just expected <- sha256 -> do
165+
contents <- LBS.readFile tmpFile
166+
let actual = SHA256.hashlazy contents
167+
unless (actual == expected) $
168+
die' verbosity $ unwords
169+
[ "Failed to download", show uri
170+
, ": SHA256 don't match; expected:", BS8.unpack (Base16.encode expected)
171+
, "actual:", BS8.unpack (Base16.encode actual)
172+
]
173+
123174
(200, Just newEtag) -> writeFile etagPath newEtag
124175
_ -> return ()
125176

@@ -131,9 +182,20 @@ downloadURI transport verbosity uri path = do
131182
304 -> do
132183
notice verbosity "Skipping download: local and remote files match."
133184
return FileAlreadyInCache
134-
errCode -> die' verbosity $ "Failed to download " ++ show uri
185+
errCode -> die' verbosity $ "failed to download " ++ show uri
135186
++ " : HTTP code " ++ show errCode
136187

188+
etagPath = path <.> "etag"
189+
uriFrag = uriFragment uri
190+
191+
sha256parsed :: Either String BS.ByteString
192+
sha256parsed = explicitEitherParsec fragmentParser uriFrag
193+
194+
fragmentParser = do
195+
_ <- P.string "#sha256="
196+
str <- some P.hexDigit
197+
return (fst (Base16.decode (BS8.pack str)))
198+
137199
------------------------------------------------------------------------------
138200
-- Utilities for repo url management
139201
--
@@ -463,7 +525,7 @@ wgetTransport prog =
463525
\responseFile responseHandle -> do
464526
hClose responseHandle
465527
(body, boundary) <- generateMultipartBody path
466-
BS.hPut tmpHandle body
528+
LBS.hPut tmpHandle body
467529
hClose tmpHandle
468530
let args = [ "--post-file=" ++ tmpFile
469531
, "--user-agent=" ++ userAgent
@@ -586,7 +648,7 @@ powershellTransport prog =
586648
withTempFile (takeDirectory path)
587649
(takeFileName path) $ \tmpFile tmpHandle -> do
588650
(body, boundary) <- generateMultipartBody path
589-
BS.hPut tmpHandle body
651+
LBS.hPut tmpHandle body
590652
hClose tmpHandle
591653
fullPath <- canonicalizePath tmpFile
592654

@@ -736,7 +798,7 @@ plainHttpTransport =
736798
rqHeaders = [ Header HdrIfNoneMatch t
737799
| t <- maybeToList etag ]
738800
++ reqHeaders,
739-
rqBody = BS.empty
801+
rqBody = LBS.empty
740802
}
741803
(_, resp) <- cabalBrowse verbosity Nothing (request req)
742804
let code = convertRspCode (rspCode resp)
@@ -752,7 +814,7 @@ plainHttpTransport =
752814
(body, boundary) <- generateMultipartBody path
753815
let headers = [ Header HdrContentType
754816
("multipart/form-data; boundary="++boundary)
755-
, Header HdrContentLength (show (BS.length body))
817+
, Header HdrContentLength (show (LBS8.length body))
756818
, Header HdrAccept ("text/plain")
757819
]
758820
req = Request {
@@ -765,11 +827,11 @@ plainHttpTransport =
765827
return (convertRspCode (rspCode resp), rspErrorString resp)
766828

767829
puthttpfile verbosity uri path auth headers = do
768-
body <- BS.readFile path
830+
body <- LBS8.readFile path
769831
let req = Request {
770832
rqURI = uri,
771833
rqMethod = PUT,
772-
rqHeaders = Header HdrContentLength (show (BS.length body))
834+
rqHeaders = Header HdrContentLength (show (LBS8.length body))
773835
: Header HdrAccept "text/plain"
774836
: headers,
775837
rqBody = body
@@ -783,7 +845,7 @@ plainHttpTransport =
783845
case lookupHeader HdrContentType (rspHeaders resp) of
784846
Just contenttype
785847
| takeWhile (/= ';') contenttype == "text/plain"
786-
-> BS.unpack (rspBody resp)
848+
-> LBS8.unpack (rspBody resp)
787849
_ -> rspReason resp
788850

789851
cabalBrowse verbosity auth act = do
@@ -829,17 +891,17 @@ trim = f . f
829891
-- Multipart stuff partially taken from cgi package.
830892
--
831893

832-
generateMultipartBody :: FilePath -> IO (BS.ByteString, String)
894+
generateMultipartBody :: FilePath -> IO (LBS.ByteString, String)
833895
generateMultipartBody path = do
834-
content <- BS.readFile path
896+
content <- LBS.readFile path
835897
boundary <- genBoundary
836-
let !body = formatBody content (BS.pack boundary)
898+
let !body = formatBody content (LBS8.pack boundary)
837899
return (body, boundary)
838900
where
839901
formatBody content boundary =
840-
BS.concat $
902+
LBS8.concat $
841903
[ crlf, dd, boundary, crlf ]
842-
++ [ BS.pack (show header) | header <- headers ]
904+
++ [ LBS8.pack (show header) | header <- headers ]
843905
++ [ crlf
844906
, content
845907
, crlf, dd, boundary, dd, crlf ]
@@ -851,8 +913,8 @@ generateMultipartBody path = do
851913
, Header HdrContentType "application/x-gzip"
852914
]
853915

854-
crlf = BS.pack "\r\n"
855-
dd = BS.pack "--"
916+
crlf = LBS8.pack "\r\n"
917+
dd = LBS8.pack "--"
856918

857919
genBoundary :: IO String
858920
genBoundary = do

0 commit comments

Comments
 (0)