1 {-# LANGUAGE BangPatterns #-}
4 -----------------------------------------------------------------------------
6 -----------------------------------------------------------------------------
8 -- | Separate module for HTTP actions, using a proxy server if one exists.
9 module Distribution
.Client
.HttpUtils
16 , remoteRepoCheckHttps
17 , remoteRepoTryUpgradeToHttps
21 import Distribution
.Client
.Compat
.Prelude
hiding (Proxy
(..))
22 import Distribution
.Utils
.Generic
25 import qualified Control
.Exception
as Exception
26 import Distribution
.Client
.Types
30 import Distribution
.Client
.Types
.Credentials
(Auth
)
31 import Distribution
.Client
.Utils
34 import Distribution
.Client
.Version
37 import Distribution
.Simple
.Program
40 , ProgramInvocation
(..)
41 , getProgramInvocationOutput
46 import Distribution
.Simple
.Program
.Db
49 , configureAllKnownPrograms
52 , prependProgramSearchPath
55 import Distribution
.Simple
.Program
.Run
56 ( getProgramInvocationOutputAndErrors
58 import Distribution
.Simple
.Utils
68 import Distribution
.System
72 import Distribution
.Utils
.String (trim
)
73 import Network
.Browser
91 import Network
.HTTP
.Proxy
(Proxy
(..), fetchProxy
)
97 import Numeric
(showHex
)
98 import System
.Directory
103 import System
.FilePath
108 import qualified System
.FilePath.Posix
as FilePath.Posix
117 import System
.IO.Error
118 ( isDoesNotExistError
120 import System
.Random
(randomRIO)
122 import qualified Crypto
.Hash
.SHA256
as SHA256
123 import qualified Data
.ByteString
as BS
124 import qualified Data
.ByteString
.Base16
as Base16
125 import qualified Data
.ByteString
.Char8
as BS8
126 import qualified Data
.ByteString
.Lazy
as LBS
127 import qualified Data
.ByteString
.Lazy
.Char8
as LBS8
128 import qualified Data
.Char as Char
129 import Distribution
.Client
.Errors
130 import qualified Distribution
.Compat
.CharParsing
as P
132 ------------------------------------------------------------------------------
133 -- Downloading a URI, given an HttpTransport
138 | FileDownloaded
FilePath
142 = -- | already downloaded and sha256 matches
144 |
-- | already downloaded and we have etag
146 |
-- | needs download with optional hash check
147 NeedsDownload
(Maybe BS
.ByteString
)
154 -- ^ What to download
158 downloadURI _transport verbosity uri path | uriScheme uri
== "file:" = do
159 copyFileVerbose verbosity
(uriPath uri
) path
160 return (FileDownloaded path
)
161 -- Can we store the hash of the file so we can safely return path when the
162 -- hash matches to avoid unnecessary computation?
164 downloadURI transport verbosity uri path
= do
165 targetExists
<- doesFileExist path
168 -- if we have uriFrag, then we expect there to be #sha256=...
169 if not (null uriFrag
)
170 then case sha256parsed
of
171 -- we know the hash, and target exists
172 Right expected | targetExists
-> do
173 contents
<- LBS
.readFile path
174 let actual
= SHA256
.hashlazy contents
175 if expected
== actual
176 then return Downloaded
177 else return (NeedsDownload
(Just expected
))
179 -- we known the hash, target doesn't exist
180 Right expected
-> return (NeedsDownload
(Just expected
))
181 -- we failed to parse uriFragment
183 dieWithException verbosity
$ CannotParseURIFragment uriFrag err
184 else -- if there are no uri fragment, use ETag
186 etagPathExists
<- doesFileExist etagPath
187 -- In rare cases the target file doesn't exist, but the etag does.
188 if targetExists
&& etagPathExists
189 then return (CheckETag etagPath
)
190 else return (NeedsDownload Nothing
)
192 -- Only use the external http transports if we actually have to
193 -- (or have been told to do so)
195 | uriScheme uri
== "http:"
196 , not (transportManuallySelected transport
) =
201 case downloadCheck
of
202 Downloaded
-> return FileAlreadyInCache
203 CheckETag etag
-> makeDownload transport
' Nothing
(Just etag
)
204 NeedsDownload hash
-> makeDownload transport
' hash Nothing
206 makeDownload
:: HttpTransport
-> Maybe BS8
.ByteString
-> Maybe String -> IO DownloadResult
207 makeDownload transport
' sha256 etag
= withTempFileName
(takeDirectory path
) (takeFileName path
) $ \tmpFile
-> do
208 result
<- getHttp transport
' verbosity uri etag tmpFile
[]
210 -- Only write the etag if we get a 200 response code.
211 -- A 304 still sends us an etag header.
213 -- if we have hash, we don't care about etag.
214 (200, _
) | Just expected
<- sha256
-> do
215 contents
<- LBS
.readFile tmpFile
216 let actual
= SHA256
.hashlazy contents
217 unless (actual
== expected
) $
218 dieWithException verbosity
$
219 MakeDownload uri expected actual
220 (200, Just newEtag
) -> writeFile etagPath newEtag
225 info verbosity
("Downloaded to " ++ path
)
226 renameFile tmpFile path
227 return (FileDownloaded path
)
229 notice verbosity
"Skipping download: local and remote files match."
230 return FileAlreadyInCache
232 dieWithException verbosity
$ FailedToDownloadURI uri
(show errCode
)
234 etagPath
= path
<.> "etag"
235 uriFrag
= uriFragment uri
237 sha256parsed
:: Either String BS
.ByteString
238 sha256parsed
= explicitEitherParsec fragmentParser uriFrag
241 _
<- P
.string "#sha256="
242 str
<- some P
.hexDigit
243 let bs
= Base16
.decode
(BS8
.pack str
)
244 #if MIN_VERSION_base16_bytestring
(1,0,0)
245 either fail return bs
250 ------------------------------------------------------------------------------
251 -- Utilities for repo url management
254 remoteRepoCheckHttps
:: Verbosity
-> HttpTransport
-> RemoteRepo
-> IO ()
255 remoteRepoCheckHttps verbosity transport repo
256 | uriScheme
(remoteRepoURI repo
) == "https:"
257 , not (transportSupportsHttps transport
) =
258 dieWithException verbosity
$ RemoteRepoCheckHttps
(unRepoName
(remoteRepoName repo
)) requiresHttpsErrorMessage
259 |
otherwise = return ()
261 transportCheckHttps
:: Verbosity
-> HttpTransport
-> URI
-> IO ()
262 transportCheckHttps verbosity transport uri
263 | uriScheme uri
== "https:"
264 , not (transportSupportsHttps transport
) =
265 dieWithException verbosity
$ TransportCheckHttps uri requiresHttpsErrorMessage
266 |
otherwise = return ()
268 requiresHttpsErrorMessage
:: String
269 requiresHttpsErrorMessage
=
270 "requires HTTPS however the built-in HTTP implementation "
271 ++ "does not support HTTPS. The transport implementations with HTTPS "
275 [name |
(name
, _
, True, _
) <- supportedTransports
]
276 ++ ". One of these will be selected automatically if the corresponding "
277 ++ "external program is available, or one can be selected specifically "
278 ++ "with the global flag --http-transport="
280 remoteRepoTryUpgradeToHttps
:: Verbosity
-> HttpTransport
-> RemoteRepo
-> IO RemoteRepo
281 remoteRepoTryUpgradeToHttps verbosity transport repo
282 | remoteRepoShouldTryHttps repo
283 , uriScheme
(remoteRepoURI repo
) == "http:"
284 , not (transportSupportsHttps transport
)
285 , not (transportManuallySelected transport
) =
286 dieWithException verbosity
$ TryUpgradeToHttps
[name |
(name
, _
, True, _
) <- supportedTransports
]
287 | remoteRepoShouldTryHttps repo
288 , uriScheme
(remoteRepoURI repo
) == "http:"
289 , transportSupportsHttps transport
=
292 { remoteRepoURI
= (remoteRepoURI repo
){uriScheme
= "https:"}
297 -- | Utility function for legacy support.
298 isOldHackageURI
:: URI
-> Bool
299 isOldHackageURI uri
=
300 case uriAuthority uri
of
301 Just
(URIAuth
{uriRegName
= "hackage.haskell.org"}) ->
302 FilePath.Posix
.splitDirectories
(uriPath uri
)
303 == ["/", "packages", "archive"]
306 ------------------------------------------------------------------------------
307 -- Setting up a HttpTransport
310 data HttpTransport
= HttpTransport
317 -> IO (HttpCode
, Maybe ETag
)
318 -- ^ GET a URI, with an optional ETag (to do a conditional fetch),
319 -- write the resource to the given file and return the HTTP status code,
320 -- and optional ETag.
326 -> IO (HttpCode
, String)
327 -- ^ POST a resource to a URI, with optional 'Auth'
328 -- and return the HTTP status code and any redirect URL.
334 -> IO (HttpCode
, String)
335 -- ^ POST a file resource to a URI using multipart\/form-data encoding,
336 -- with optional 'Auth' and return the HTTP status
337 -- code and any error string.
344 -> IO (HttpCode
, String)
345 -- ^ PUT a file resource to a URI, with optional 'Auth',
346 -- extra headers and return the HTTP status code
347 -- and any error string.
348 , transportSupportsHttps
:: Bool
349 -- ^ Whether this transport supports https or just http.
350 , transportManuallySelected
:: Bool
351 -- ^ Whether this transport implementation was specifically chosen by
352 -- the user via configuration, or whether it was automatically selected.
353 -- Strictly speaking this is not a property of the transport itself but
354 -- about how it was chosen. Nevertheless it's convenient to keep here.
357 -- TODO: why does postHttp return a redirect, but postHttpFile return errors?
368 noPostYet verbosity _ _ _
= dieWithException verbosity NoPostYet
374 , ProgramDb
-> Maybe HttpTransport
377 supportedTransports
=
378 [ let prog
= simpleProgram
"curl"
382 , \db
-> curlTransport
<$> lookupProgram prog db
384 , let prog
= simpleProgram
"wget"
388 , \db
-> wgetTransport
<$> lookupProgram prog db
390 , let prog
= simpleProgram
"powershell"
394 , \db
-> powershellTransport
<$> lookupProgram prog db
400 , \_
-> Just plainHttpTransport
404 configureTransport
:: Verbosity
-> [FilePath] -> Maybe String -> IO HttpTransport
405 configureTransport verbosity extraPath
(Just name
) =
406 -- the user specifically selected a transport by name so we'll try and
407 -- configure that one
409 case find (\(name
', _
, _
, _
) -> name
' == name
) supportedTransports
of
410 Just
(_
, mprog
, _tls
, mkTrans
) -> do
411 baseProgDb
<- prependProgramSearchPath verbosity extraPath
[] emptyProgramDb
412 progdb
<- case mprog
of
413 Nothing
-> return emptyProgramDb
414 Just prog
-> snd <$> requireProgram verbosity prog baseProgDb
415 -- ^^ if it fails, it'll fail here
417 let transport
= fromMaybe (error "configureTransport: failed to make transport") $ mkTrans progdb
418 return transport
{transportManuallySelected
= True}
420 dieWithException verbosity
$ UnknownHttpTransportSpecified name
[name
' |
(name
', _
, _
, _
) <- supportedTransports
]
421 configureTransport verbosity extraPath Nothing
= do
422 -- the user hasn't selected a transport, so we'll pick the first one we
423 -- can configure successfully, provided that it supports tls
425 -- for all the transports except plain-http we need to try and find
426 -- their external executable
427 baseProgDb
<- prependProgramSearchPath verbosity extraPath
[] emptyProgramDb
429 configureAllKnownPrograms verbosity
$
431 [prog |
(_
, Just prog
, _
, _
) <- supportedTransports
]
434 let availableTransports
=
436 |
(name
, _
, _
, mkTrans
) <- supportedTransports
437 , transport
<- maybeToList (mkTrans progdb
)
439 let (name
, transport
) =
440 fromMaybe ("plain-http", plainHttpTransport
) (safeHead availableTransports
)
441 debug verbosity
$ "Selected http transport implementation: " ++ name
443 return transport
{transportManuallySelected
= False}
445 ------------------------------------------------------------------------------
446 -- The HttpTransports based on external programs
449 curlTransport
:: ConfiguredProgram
-> HttpTransport
451 HttpTransport gethttp posthttp posthttpfile puthttpfile
True False
453 gethttp verbosity uri etag destPath reqHeaders
= do
455 (takeDirectory destPath
)
457 $ \tmpFile tmpHandle
-> do
474 [ ["--header", "If-None-Match: " ++ t
]
475 | t
<- maybeToList etag
478 [ ["--header", show name
++ ": " ++ value]
479 | Header name
value <- reqHeaders
483 getProgramInvocationOutput verbosity
$
487 (programInvocation prog args
)
489 withFile tmpFile ReadMode
$ \hnd
-> do
490 headers
<- hGetContents hnd
491 (code
, _err
, etag
') <- parseResponse verbosity uri resp headers
492 evaluate
$ force
(code
, etag
')
496 addAuthConfig explicitAuth uri progInvocation
= do
497 -- attempt to derive a u/p pair from the uri authority if one exists
498 -- all `uriUserInfo` values have '@' as a suffix. drop it.
499 let uriDerivedAuth
= case uriAuthority uri
of
500 (Just
(URIAuth u _ _
)) |
not (null u
) -> Just
$ filter (/= '@') u
502 -- prefer passed in auth to auth derived from uri. If neither exist, then no auth
503 let mbAuthStringToken
= case (explicitAuth
, uriDerivedAuth
) of
504 (Just
(Right token
), _
) -> Just
$ Right token
505 (Just
(Left
(uname
, passwd
)), _
) -> Just
$ Left
(uname
++ ":" ++ passwd
)
506 (Nothing
, Just a
) -> Just
$ Left a
507 (Nothing
, Nothing
) -> Nothing
508 case mbAuthStringToken
of
512 Just
. IODataText
. unlines $
516 , progInvokeArgs
= ["--config", "-"] ++ progInvokeArgs progInvocation
518 Just
(Right token
) ->
521 ["--header", "Authorization: X-ApiKey " ++ token
]
522 ++ progInvokeArgs progInvocation
524 Nothing
-> progInvocation
526 posthttpfile verbosity uri path auth
= do
530 , "package=@" ++ path
538 , "Accept: text/plain"
542 getProgramInvocationOutput verbosity
$
546 (programInvocation prog args
)
547 (code
, err
, _etag
) <- parseResponse verbosity uri resp
""
550 puthttpfile verbosity uri path auth headers
= do
565 , "Accept: text/plain"
568 [ ["--header", show name
++ ": " ++ value]
569 | Header name
value <- headers
572 getProgramInvocationOutput verbosity
$
576 (programInvocation prog args
)
577 (code
, err
, _etag
) <- parseResponse verbosity uri resp
""
580 -- on success these curl invocations produces an output like "200"
581 -- and on failure it has the server error response first
582 parseResponse
:: Verbosity
-> URI
-> String -> String -> IO (Int, String, Maybe ETag
)
583 parseResponse verbosity uri resp headers
=
585 case reverse (lines resp
) of
586 (codeLine
: rerrLines
) ->
587 case readMaybe
(trim codeLine
) of
589 let errstr
= mkErrstr rerrLines
594 mkErrstr
= unlines . reverse . dropWhile (all isSpace)
596 mb_etag
:: Maybe ETag
601 |
[name
, etag
] <- map words (lines headers
)
605 Just
(i
, err
) -> return (i
, err
, mb_etag
)
606 _
-> statusParseFail verbosity uri resp
608 wgetTransport
:: ConfiguredProgram
-> HttpTransport
610 HttpTransport gethttp posthttp posthttpfile puthttpfile
True False
612 gethttp verbosity uri etag destPath reqHeaders
= do
613 resp
<- runWGet verbosity uri args
615 -- wget doesn't support range requests.
616 -- so, we not only ignore range request headers,
617 -- but we also display a warning message when we see them.
618 let hasRangeHeader
= any isRangeHeader reqHeaders
620 "the 'wget' transport currently doesn't support"
621 ++ " range requests, which wastes network bandwidth."
622 ++ " To fix this, set 'http-transport' to 'curl' or"
623 ++ " 'plain-http' in '~/.config/cabal/config'."
624 ++ " Note that the 'plain-http' transport doesn't"
625 ++ " support HTTPS.\n"
627 when (hasRangeHeader
) $ warn verbosity warningMsg
628 (code
, etag
') <- parseOutput verbosity uri resp
632 [ "--output-document=" ++ destPath
633 , "--user-agent=" ++ userAgent
636 , "--server-response"
639 [ ["--header", "If-None-Match: " ++ t
]
640 | t
<- maybeToList etag
642 ++ [ "--header=" ++ show name
++ ": " ++ value
643 | hdr
@(Header name
value) <- reqHeaders
644 , (not (isRangeHeader hdr
))
647 -- wget doesn't support range requests.
648 -- so, we ignore range request headers, lest we get errors.
649 isRangeHeader
:: Header
-> Bool
650 isRangeHeader
(Header HdrRange _
) = True
651 isRangeHeader _
= False
655 posthttpfile verbosity uri path auth
=
659 $ \tmpFile tmpHandle
->
660 withTempFile
(takeDirectory path
) "response" $
661 \responseFile responseHandle
-> do
662 hClose responseHandle
663 (body
, boundary
) <- generateMultipartBody path
664 LBS
.hPut tmpHandle body
667 [ "--post-file=" ++ tmpFile
668 , "--user-agent=" ++ userAgent
669 , "--server-response"
670 , "--output-document=" ++ responseFile
671 , "--header=Accept: text/plain"
672 , "--header=Content-type: multipart/form-data; "
676 ++ maybeToList (authTokenHeader auth
)
677 out
<- runWGet verbosity
(addUriAuth auth uri
) args
678 (code
, _etag
) <- parseOutput verbosity uri out
679 withFile responseFile ReadMode
$ \hnd
-> do
680 resp
<- hGetContents hnd
681 evaluate
$ force
(code
, resp
)
683 puthttpfile verbosity uri path auth headers
=
684 withTempFile
(takeDirectory path
) "response" $
685 \responseFile responseHandle
-> do
686 hClose responseHandle
689 , "--body-file=" ++ path
690 , "--user-agent=" ++ userAgent
691 , "--server-response"
692 , "--output-document=" ++ responseFile
693 , "--header=Accept: text/plain"
695 ++ [ "--header=" ++ show name
++ ": " ++ value
696 | Header name
value <- headers
698 ++ maybeToList (authTokenHeader auth
)
700 out
<- runWGet verbosity
(addUriAuth auth uri
) args
701 (code
, _etag
) <- parseOutput verbosity uri out
702 withFile responseFile ReadMode
$ \hnd
-> do
703 resp
<- hGetContents hnd
704 evaluate
$ force
(code
, resp
)
706 authTokenHeader
(Just
(Right token
)) = Just
$ "--header=Authorization: X-ApiKey " ++ token
707 authTokenHeader _
= Nothing
709 addUriAuth
(Just
(Left
(user
, pass
))) uri
=
711 { uriAuthority
= Just a
{uriUserInfo
= user
++ ":" ++ pass
++ "@"}
714 a
= fromMaybe (URIAuth
"" "" "") (uriAuthority uri
)
715 addUriAuth _ uri
= uri
717 runWGet verbosity uri args
= do
718 -- We pass the URI via STDIN because it contains the users' credentials
719 -- and sensitive data should not be passed via command line arguments.
722 (programInvocation prog
("--input-file=-" : args
))
723 { progInvokeInput
= Just
$ IODataText
$ uriToString
id uri
""
726 -- wget returns its output on stderr rather than stdout
727 (_
, resp
, exitCode
) <-
728 getProgramInvocationOutputAndErrors
731 -- wget returns exit code 8 for server "errors" like "304 not modified"
732 if exitCode
== ExitSuccess || exitCode
== ExitFailure
8
734 else dieWithException verbosity
$ WGetServerError
(programPath prog
) resp
736 -- With the --server-response flag, wget produces output with the full
737 -- http server response with all headers, we want to find a line like
738 -- "HTTP/1.1 200 OK", but only the last one, since we can have multiple
739 -- requests due to redirects.
740 parseOutput verbosity uri resp
=
744 |
(protocol
: codestr
: _err
) <- map words (reverse (lines resp
))
745 , "HTTP/" `
isPrefixOf` protocol
746 , code
<- maybeToList (readMaybe codestr
)
748 mb_etag
:: Maybe ETag
752 |
[name
, etag
] <- map words (reverse (lines resp
))
755 in case parsedCode
of
756 Just i
-> return (i
, mb_etag
)
757 _
-> statusParseFail verbosity uri resp
759 powershellTransport
:: ConfiguredProgram
-> HttpTransport
760 powershellTransport prog
=
761 HttpTransport gethttp posthttp posthttpfile puthttpfile
True False
763 gethttp verbosity uri etag destPath reqHeaders
= do
765 runPowershellScript verbosity
$
768 ( ("$targetStream = New-Object -TypeName System.IO.FileStream -ArgumentList " ++ (escape destPath
) ++ ", Create")
769 : (setupHeaders
((useragentHeader
: etagHeader
) ++ reqHeaders
))
771 [ "$response = $request.GetResponse()"
772 , "$responseStream = $response.GetResponseStream()"
773 , "$buffer = new-object byte[] 10KB"
774 , "$count = $responseStream.Read($buffer, 0, $buffer.length)"
775 , "while ($count -gt 0)"
777 , " $targetStream.Write($buffer, 0, $count)"
778 , " $count = $responseStream.Read($buffer, 0, $buffer.length)"
780 , "Write-Host ($response.StatusCode -as [int]);"
781 , "Write-Host $response.GetResponseHeader(\"ETag\").Trim('\"')"
783 [ "$targetStream.Flush()"
784 , "$targetStream.Close()"
785 , "$targetStream.Dispose()"
786 , "$responseStream.Dispose()"
790 parseResponse
:: String -> IO (HttpCode
, Maybe ETag
)
792 case lines $ trim x
of
793 (code
: etagv
: _
) -> fmap (\c
-> (c
, Just etagv
)) $ parseCode code x
794 (code
: _
) -> fmap (\c
-> (c
, Nothing
)) $ parseCode code x
795 _
-> statusParseFail verbosity uri x
796 parseCode
:: String -> String -> IO HttpCode
797 parseCode code x
= case readMaybe code
of
799 Nothing
-> statusParseFail verbosity uri x
800 etagHeader
= [Header HdrIfNoneMatch t | t
<- maybeToList etag
]
804 posthttpfile verbosity uri path auth
=
808 $ \tmpFile tmpHandle
-> do
809 (body
, boundary
) <- generateMultipartBody path
810 LBS
.hPut tmpHandle body
812 fullPath
<- canonicalizePath tmpFile
817 ("multipart/form-data; boundary=" ++ boundary
)
819 runPowershellScript verbosity
$
822 (setupHeaders
(contentHeader
: extraHeaders
) ++ setupAuth auth
)
823 (uploadFileAction
"POST" uri fullPath
)
825 parseUploadResponse verbosity uri resp
827 puthttpfile verbosity uri path auth headers
= do
828 fullPath
<- canonicalizePath path
830 runPowershellScript verbosity
$
833 (setupHeaders
(extraHeaders
++ headers
) ++ setupAuth auth
)
834 (uploadFileAction
"PUT" uri fullPath
)
836 parseUploadResponse verbosity uri resp
838 runPowershellScript verbosity script
= do
842 , -- the default execution policy doesn't allow running
843 -- unsigned scripts, so we need to tell powershell to bypass it
851 debug verbosity script
852 getProgramInvocationOutput
854 (programInvocation prog args
)
855 { progInvokeInput
= Just
$ IODataText
$ script
++ "\nExit(0);"
860 useragentHeader
= Header HdrUserAgent userAgent
861 extraHeaders
= [Header HdrAccept
"text/plain", useragentHeader
]
863 setupHeaders headers
=
864 [ "$request." ++ addHeader name
value
865 | Header name
value <- headers
868 addHeader header
value =
870 HdrAccept
-> "Accept = " ++ escape
value
871 HdrUserAgent
-> "UserAgent = " ++ escape
value
872 HdrConnection
-> "Connection = " ++ escape
value
873 HdrContentLength
-> "ContentLength = " ++ escape
value
874 HdrContentType
-> "ContentType = " ++ escape
value
875 HdrDate
-> "Date = " ++ escape
value
876 HdrExpect
-> "Expect = " ++ escape
value
877 HdrHost
-> "Host = " ++ escape
value
878 HdrIfModifiedSince
-> "IfModifiedSince = " ++ escape
value
879 HdrReferer
-> "Referer = " ++ escape
value
880 HdrTransferEncoding
-> "TransferEncoding = " ++ escape
value
883 if "bytes=" `
isPrefixOf`
value
884 then case break (== '-') value' of
885 (start
', '-' : end
') -> (start
', end
')
886 _
-> error $ "Could not decode range: " ++ value
887 else error $ "Could not decode range: " ++ value
888 value' = drop 6 value
889 in "AddRange(\"bytes\", " ++ escape start
++ ", " ++ escape end
++ ");"
890 name
-> "Headers.Add(" ++ escape
(show name
) ++ "," ++ escape
value ++ ");"
892 setupAuth
(Just
(Left
(uname
, passwd
))) =
893 [ "$request.Credentials = new-object System.Net.NetworkCredential("
899 setupAuth
(Just
(Right token
)) =
900 ["$request.Headers[\"Authorization\"] = " ++ escape
("X-ApiKey " ++ token
)]
901 setupAuth Nothing
= []
903 uploadFileAction method _uri fullPath
=
904 [ "$request.Method = " ++ show method
905 , "$requestStream = $request.GetRequestStream()"
906 , "$fileStream = [System.IO.File]::OpenRead(" ++ escape fullPath
++ ")"
908 , "$chunk = New-Object byte[] $bufSize"
909 , "while( $bytesRead = $fileStream.Read($chunk,0,$bufsize) )"
911 , " $requestStream.write($chunk, 0, $bytesRead)"
912 , " $requestStream.Flush()"
915 , "$responseStream = $request.getresponse()"
916 , "$responseReader = new-object System.IO.StreamReader $responseStream.GetResponseStream()"
917 , "$code = $response.StatusCode -as [int]"
918 , "if ($code -eq 0) {"
922 , "Write-Host $responseReader.ReadToEnd()"
926 [ "$fileStream.Close()"
927 , "$requestStream.Close()"
928 , "$responseStream.Close()"
931 parseUploadResponse verbosity uri resp
= case lines (trim resp
) of
933 | Just code
<- readMaybe codeStr
-> return (code
, unlines message
)
934 _
-> statusParseFail verbosity uri resp
936 webclientScript uri setup action cleanup
=
938 [ "[Net.ServicePointManager]::SecurityProtocol = \"tls12, tls11, tls\""
939 , "$uri = New-Object \"System.Uri\" " ++ uri
940 , "$request = [System.Net.HttpWebRequest]::Create($uri)"
943 , unlines (map (" " ++) action
)
944 , "} Catch [System.Net.WebException] {"
945 , " $exception = $_.Exception;"
946 , " If ($exception.Status -eq "
947 ++ "[System.Net.WebExceptionStatus]::ProtocolError) {"
948 , " $response = $exception.Response -as [System.Net.HttpWebResponse];"
949 , " $reader = new-object "
950 ++ "System.IO.StreamReader($response.GetResponseStream());"
951 , " Write-Host ($response.StatusCode -as [int]);"
952 , " Write-Host $reader.ReadToEnd();"
954 , " Write-Host $exception.Message;"
957 , " Write-Host $_.Exception.Message;"
959 , unlines (map (" " ++) cleanup
)
963 ------------------------------------------------------------------------------
964 -- The builtin plain HttpTransport
967 plainHttpTransport
:: HttpTransport
969 HttpTransport gethttp posthttp posthttpfile puthttpfile
False False
971 gethttp verbosity uri etag destPath reqHeaders
= do
977 [ Header HdrIfNoneMatch t
978 | t
<- maybeToList etag
983 (_
, resp
) <- cabalBrowse verbosity Nothing
(request req
)
984 let code
= convertRspCode
(rspCode resp
)
985 etag
' = lookupHeader HdrETag
(rspHeaders resp
)
986 -- 206 Partial Content is a normal response to a range request; see #3385.
987 when (code
== 200 || code
== 206) $
988 writeFileAtomic destPath
$
994 posthttpfile verbosity uri path auth
= do
995 (body
, boundary
) <- generateMultipartBody path
999 ("multipart/form-data; boundary=" ++ boundary
)
1000 , Header HdrContentLength
(show (LBS8
.length body
))
1001 , Header HdrAccept
("text/plain")
1003 ++ maybeToList (authTokenHeader auth
)
1008 , rqHeaders
= headers
1011 (_
, resp
) <- cabalBrowse verbosity auth
(request req
)
1012 return (convertRspCode
(rspCode resp
), rspErrorString resp
)
1014 puthttpfile verbosity uri path auth headers
= do
1015 body
<- LBS8
.readFile path
1021 Header HdrContentLength
(show (LBS8
.length body
))
1022 : Header HdrAccept
"text/plain"
1023 : maybeToList (authTokenHeader auth
)
1027 (_
, resp
) <- cabalBrowse verbosity auth
(request req
)
1028 return (convertRspCode
(rspCode resp
), rspErrorString resp
)
1030 convertRspCode
(a
, b
, c
) = a
* 100 + b
* 10 + c
1032 rspErrorString resp
=
1033 case lookupHeader HdrContentType
(rspHeaders resp
) of
1035 |
takeWhile (/= ';') contenttype
== "text/plain" ->
1036 LBS8
.unpack
(rspBody resp
)
1039 cabalBrowse verbosity auth act
= do
1040 p
<- fixupEmptyProxy
<$> fetchProxy
True
1041 Exception
.handleJust
1042 (guard . isDoesNotExistError)
1043 ( const . dieWithException verbosity
$ Couldn
'tEstablishHttpConnection
1048 setErrHandler
(warn verbosity
. ("http error: " ++))
1049 setOutHandler
(debug verbosity
)
1050 setUserAgent userAgent
1051 setAllowBasicAuth
False
1053 Just
(Left x
) -> setAuthorityGen
(\_ _
-> return $ Just x
)
1054 _
-> setAuthorityGen
(\_ _
-> return Nothing
)
1057 authTokenHeader
(Just
(Right token
)) = Just
$ Header HdrAuthorization
("X-ApiKey " ++ token
)
1058 authTokenHeader _
= Nothing
1060 fixupEmptyProxy
(Proxy uri _
) |
null uri
= NoProxy
1061 fixupEmptyProxy p
= p
1063 ------------------------------------------------------------------------------
1064 -- Common stuff used by multiple transport impls
1071 , prettyShow cabalInstallVersion
1073 , prettyShow buildOS
1075 , prettyShow buildArch
1079 statusParseFail
:: Verbosity
-> URI
-> String -> IO a
1080 statusParseFail verbosity uri r
=
1081 dieWithException verbosity
$ StatusParseFail uri r
1083 ------------------------------------------------------------------------------
1084 -- Multipart stuff partially taken from cgi package.
1087 generateMultipartBody
:: FilePath -> IO (LBS
.ByteString
, String)
1088 generateMultipartBody path
= do
1089 content
<- LBS
.readFile path
1090 boundary
<- genBoundary
1091 let !body
= formatBody content
(LBS8
.pack boundary
)
1092 return (body
, boundary
)
1094 formatBody content boundary
=
1096 [crlf
, dd
, boundary
, crlf
]
1097 ++ [LBS8
.pack
(show header
) | header
<- headers
]
1109 (HdrCustom
"Content-disposition")
1110 ( "form-data; name=package; "
1112 ++ takeFileName path
1115 , Header HdrContentType
"application/x-gzip"
1118 crlf
= LBS8
.pack
"\r\n"
1121 genBoundary
:: IO String
1123 i
<- randomRIO (0x10000000000000, 0xFFFFFFFFFFFFFF) :: IO Integer
1124 return $ showHex i
""
1126 isETag
:: String -> Bool
1127 isETag name
= fmap Char.toLower name
== "etag:"