From e012329489bb1c9ee6360e44625c3804027228aa Mon Sep 17 00:00:00 2001 From: Sam Hanes Date: Thu, 24 Aug 2017 11:29:40 -0400 Subject: [PATCH] Add support for ntlm authentication --- docs/content/commands/config.md | 4 +- docs/content/nuget-dependencies.md | 8 ++- src/Paket.Core/Common/Utils.fs | 19 +++++-- src/Paket.Core/Dependencies/NuGet.fs | 13 +++-- src/Paket.Core/Dependencies/NuGetV2.fs | 2 +- src/Paket.Core/Dependencies/NuGetV3.fs | 6 +-- src/Paket.Core/Dependencies/RemoteUpload.fs | 2 +- .../PackageManagement/NugetConvert.fs | 18 +++---- src/Paket.Core/PublicAPI.fs | 4 +- src/Paket.Core/Versioning/ConfigFile.fs | 29 ++++++++--- src/Paket.Core/Versioning/PackageSources.fs | 46 ++++++++++------- src/Paket/Commands.fs | 2 + src/Paket/Program.fs | 3 +- .../DependenciesFile/ParserSpecs.fs | 50 ++++++++++++++++--- .../NuGetConfig/NuGetConfigSpecs.fs | 4 +- .../Paket.Tests/Versioning/ConfigFileSpecs.fs | 20 +++++++- 16 files changed, 163 insertions(+), 67 deletions(-) diff --git a/docs/content/commands/config.md b/docs/content/commands/config.md index 9fc74f42aa..10b501d8bc 100644 --- a/docs/content/commands/config.md +++ b/docs/content/commands/config.md @@ -6,8 +6,8 @@ paket config add-credentials ``` -Paket will then ask you for the username and password that will be used for the -specified ``. +Paket will then ask you for the username, password, and authentication type that +will be used for the specified ``. The credentials you enter here will then be used for `source`s in the [`paket.dependencies` file](nuget-dependencies.html) that match `` diff --git a/docs/content/nuget-dependencies.md b/docs/content/nuget-dependencies.md index 73d6d307ed..fd53cb0723 100644 --- a/docs/content/nuget-dependencies.md +++ b/docs/content/nuget-dependencies.md @@ -36,14 +36,14 @@ source http://myserver/nuget/api/v2 // Custom feed. It's also possible to provide login information for private NuGet feeds: ```paket -source http://example.com/nuget/api/v2 username: "user name" password: "the password" +source http://example.com/nuget/api/v2 username: "user name" password: "the password" authtype: "basic" ``` If you don't want to check your username and password into source control, you can use environment variables instead: ```paket -source http://myserver/nuget/api/v2 username: "%PRIVATE_FEED_USER%" password: "%PRIVATE_FEED_PASS%" +source http://myserver/nuget/api/v2 username: "%PRIVATE_FEED_USER%" password: "%PRIVATE_FEED_PASS%" authtype: "ntlm" ``` `%PRIVATE_FEED_USER%` and `%PRIVATE_FEED_PASS%` will be expanded with the @@ -52,6 +52,10 @@ variables. The [`paket.lock` file](lock-file.html) will also reflect these settings. +`authtype` is an optional parameter to specify the authentication scheme. Allowed +values are `basic` and `ntlm`. If no authentication type is specified, basic +authentication will be used. + **Note:** If [`paket.dependencies` file](dependencies-file.html) exists while running the [`convert-from-nuget` command](paket-convert-from-nuget.html), the `PRIVATE_FEED_USER` and `PRIVATE_FEED_PASS` will *not* be expanded. Please see diff --git a/src/Paket.Core/Common/Utils.fs b/src/Paket.Core/Common/Utils.fs index 3093548d99..a4f6ee8031 100644 --- a/src/Paket.Core/Common/Utils.fs +++ b/src/Paket.Core/Common/Utils.fs @@ -98,10 +98,17 @@ let internal memoizeAsync f = fun (x: 'a) -> // task.Result serialization to sync after done. cache.GetOrAdd(x, fun x -> f(x) |> Async.StartAsTask) |> Async.AwaitTask +type AuthType = | Basic | NTLM + type Auth = - | Credentials of Username : string * Password : string + | Credentials of Username : string * Password : string * Type : AuthType | Token of string +let internal parseAuthTypeString (str:string) = + match str.Trim().ToLowerInvariant() with + | "ntlm" -> AuthType.NTLM + | _ -> AuthType.Basic + let TimeSpanToReadableString(span:TimeSpan) = let pluralize x = if x = 1 then String.Empty else "s" let notZero x y = if x > 0 then y else String.Empty @@ -582,7 +589,7 @@ let createHttpClient (url,auth:Auth option) = let client = new HttpClient(handler) match auth with | None -> handler.UseDefaultCredentials <- true - | Some(Credentials(username, password)) -> + | Some(Credentials(username, password, AuthType.Basic)) -> // htttp://stackoverflow.com/questions/16044313/webclient-httpwebrequest-with-basic-authentication-returns-404-not-found-for-v/26016919#26016919 //this works ONLY if the server returns 401 first //client DOES NOT send credentials on first request @@ -593,6 +600,9 @@ let createHttpClient (url,auth:Auth option) = let credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password)) client.DefaultRequestHeaders.Authorization <- new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials) + | Some(Credentials(username, password, AuthType.NTLM)) -> + let cred = System.Net.NetworkCredential(username,password) + handler.Credentials <- cred.GetCredential(new Uri(url), "NTLM") | Some(Token token) -> client.DefaultRequestHeaders.Authorization <- new System.Net.Http.Headers.AuthenticationHeaderValue("token", token) @@ -616,7 +626,7 @@ let createWebClient (url,auth:Auth option) = let githubToken = Environment.GetEnvironmentVariable "PAKET_GITHUB_API_TOKEN" match auth with - | Some (Credentials(username, password)) -> + | Some (Credentials(username, password, AuthType.Basic)) -> // htttp://stackoverflow.com/questions/16044313/webclient-httpwebrequest-with-basic-authentication-returns-404-not-found-for-v/26016919#26016919 //this works ONLY if the server returns 401 first //client DOES NOT send credentials on first request @@ -627,6 +637,9 @@ let createWebClient (url,auth:Auth option) = let credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password)) client.Headers.[HttpRequestHeader.Authorization] <- sprintf "Basic %s" credentials client.Credentials <- new NetworkCredential(username,password) + | Some (Credentials(username, password, AuthType.NTLM)) -> + let cred = NetworkCredential(username,password) + client.Credentials <- cred.GetCredential(new Uri(url), "NTLM") | Some (Token token) -> client.Headers.[HttpRequestHeader.Authorization] <- sprintf "token %s" token | None when not (isNull githubToken) -> diff --git a/src/Paket.Core/Dependencies/NuGet.fs b/src/Paket.Core/Dependencies/NuGet.fs index 7cc21219aa..2bb2213042 100644 --- a/src/Paket.Core/Dependencies/NuGet.fs +++ b/src/Paket.Core/Dependencies/NuGet.fs @@ -438,7 +438,7 @@ let GetVersions force alternativeProjectRoot root (sources, packageName:PackageN if (not force) && errorFileExists then return [] else match nugetSource with | NuGetV2 source -> - let auth = source.Authentication |> Option.map toBasicAuth + let auth = source.Authentication |> Option.map toCredentials if String.containsIgnoreCase "artifactory" source.Url then return [getVersionsCached "ODataNewestFirst" NuGetV2.tryGetAllVersionsFromNugetODataFindByIdNewestFirst (nugetSource, auth, source.Url, packageName) ] else @@ -449,7 +449,7 @@ let GetVersions force alternativeProjectRoot root (sources, packageName:PackageN return v2Feeds | NuGetV3 source -> let! versionsAPI = PackageSources.getNuGetV3Resource source AllVersionsAPI - let auth = source.Authentication |> Option.map toBasicAuth + let auth = source.Authentication |> Option.map toCredentials return [ getVersionsCached "V3" tryNuGetV3 (nugetSource, auth, versionsAPI, packageName) ] | LocalNuGet(path,Some _) -> return [ NuGetLocal.getAllVersionsFromLocalPath (true, path, packageName, alternativeProjectRoot, root) ] @@ -695,9 +695,9 @@ let DownloadPackage(alternativeProjectRoot, root, (source : PackageSource), cach #endif if authenticated then - match source.Auth |> Option.map toBasicAuth with + match source.Auth |> Option.map toCredentials with | None | Some(Token _) -> request.UseDefaultCredentials <- true - | Some(Credentials(username, password)) -> + | Some(Credentials(username, password, AuthType.Basic)) -> // htttp://stackoverflow.com/questions/16044313/webclient-httpwebrequest-with-basic-authentication-returns-404-not-found-for-v/26016919#26016919 //this works ONLY if the server returns 401 first //client DOES NOT send credentials on first request @@ -707,6 +707,9 @@ let DownloadPackage(alternativeProjectRoot, root, (source : PackageSource), cach //so use THIS instead to send credentials RIGHT AWAY let credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password)) request.Headers.[HttpRequestHeader.Authorization] <- String.Format("Basic {0}", credentials) + | Some(Credentials(username, password, AuthType.NTLM)) -> + let cred = NetworkCredential(username,password) + request.Credentials <- cred.GetCredential(downloadUri, "NTLM") else request.UseDefaultCredentials <- true @@ -743,7 +746,7 @@ let DownloadPackage(alternativeProjectRoot, root, (source : PackageSource), cach | :? System.Net.WebException as exn when attempt < 5 && exn.Status = WebExceptionStatus.ProtocolError && - (match source.Auth |> Option.map toBasicAuth with + (match source.Auth |> Option.map toCredentials with | Some(Credentials(_)) -> true | _ -> false) -> do! download false (attempt + 1) diff --git a/src/Paket.Core/Dependencies/NuGetV2.fs b/src/Paket.Core/Dependencies/NuGetV2.fs index eae835fc1f..e2c02a05b2 100644 --- a/src/Paket.Core/Dependencies/NuGetV2.fs +++ b/src/Paket.Core/Dependencies/NuGetV2.fs @@ -393,7 +393,7 @@ let FindPackages(auth, nugetURL, packageNamePrefix, maxResults) = let url = sprintf "%s/Packages()?$filter=IsLatestVersion and IsAbsoluteLatestVersion and substringof('%s',tolower(Id))" nugetURL ((packageNamePrefix:string).ToLowerInvariant()) async { try - let! raw = getFromUrl(auth |> Option.map toBasicAuth,url,acceptXml) + let! raw = getFromUrl(auth |> Option.map toCredentials,url,acceptXml) let doc = XmlDocument() doc.LoadXml raw return diff --git a/src/Paket.Core/Dependencies/NuGetV3.fs b/src/Paket.Core/Dependencies/NuGetV3.fs index 84c983b764..414ba4c0e5 100644 --- a/src/Paket.Core/Dependencies/NuGetV3.fs +++ b/src/Paket.Core/Dependencies/NuGetV3.fs @@ -160,7 +160,7 @@ let private getPackages(auth, nugetURL, packageNamePrefix, maxResults) = async { match apiRes with | Some url -> let query = sprintf "%s?q=%s&take=%d" url packageNamePrefix maxResults - let! response = safeGetFromUrl(auth |> Option.map toBasicAuth,query,acceptJson) + let! response = safeGetFromUrl(auth |> Option.map toCredentials,query,acceptJson) match SafeWebResult.asResult response with | Result.Ok text -> return Result.Ok (extractPackages text) | Result.Error err -> return Result.Error err @@ -208,7 +208,7 @@ let getRegistration (source : NugetV3Source) (packageName:PackageName) (version: async { let! registrationUrl = PackageSources.getNuGetV3Resource source Registration let url = sprintf "%s%s/%s.json" registrationUrl (packageName.ToString().ToLower()) (version.Normalize()) - let! rawData = safeGetFromUrl (source.Authentication |> Option.map toBasicAuth, url, acceptJson) + let! rawData = safeGetFromUrl (source.Authentication |> Option.map toCredentials, url, acceptJson) return match rawData with | NotFound -> None //raise <| System.Exception(sprintf "could not get registration data (404) from '%s'" url) @@ -235,7 +235,7 @@ let getPackageDetails (source:NugetV3Source) (packageName:PackageName) (version: match registrationData with | None -> return EmptyResult | Some registrationData -> - let! catalogData = getCatalog registrationData.CatalogEntry (source.Authentication |> Option.map toBasicAuth) + let! catalogData = getCatalog registrationData.CatalogEntry (source.Authentication |> Option.map toCredentials) let dependencies = if catalogData.DependencyGroups = null then diff --git a/src/Paket.Core/Dependencies/RemoteUpload.fs b/src/Paket.Core/Dependencies/RemoteUpload.fs index 7f720bcd37..25b964dfa4 100644 --- a/src/Paket.Core/Dependencies/RemoteUpload.fs +++ b/src/Paket.Core/Dependencies/RemoteUpload.fs @@ -43,7 +43,7 @@ let Push maxTrials url apiKey clientVersion packageFileName = try let authOpt = ConfigFile.GetAuthentication(url) match authOpt with - | Some (Auth.Credentials (u,_)) -> + | Some (Auth.Credentials (u,_,_)) -> tracefnVerbose "Authorizing using credentials for user %s" u | Some (Auth.Token _) -> tracefnVerbose "Authorizing using token" diff --git a/src/Paket.Core/PackageManagement/NugetConvert.fs b/src/Paket.Core/PackageManagement/NugetConvert.fs index dab78413c2..2c4050723c 100644 --- a/src/Paket.Core/PackageManagement/NugetConvert.fs +++ b/src/Paket.Core/PackageManagement/NugetConvert.fs @@ -28,19 +28,19 @@ type CredsMigrationMode = static member ToAuthentication mode sourceName auth = match mode, auth with - | Encrypt, Credentials(username,password) -> - ConfigAuthentication(username, password) - | Plaintext, Credentials(username,password) -> - PlainTextAuthentication(username, password) - | Selective, Credentials(username,password) -> + | Encrypt, Credentials(username, password, authType) -> + ConfigAuthentication(username, password, authType) + | Plaintext, Credentials(username, password, authType) -> + PlainTextAuthentication(username, password, authType) + | Selective, Credentials(username, password, authType) -> let question = sprintf "Credentials for source '%s': " sourceName + "[encrypt and save in config (Yes) " + sprintf "| save as plaintext in %s (No)]" Constants.DependenciesFileName match Utils.askYesNo question with - | true -> ConfigAuthentication(username, password) - | false -> PlainTextAuthentication(username, password) + | true -> ConfigAuthentication(username, password, authType) + | false -> PlainTextAuthentication(username, password, authType) | _ -> failwith "invalid auth" /// Represents type of NuGet packages.config file @@ -96,8 +96,8 @@ type NugetConfig = let encryptedPass = authNode |> tryGetValue "Password" match userName, encryptedPass, clearTextPass with - | Some userName, Some encryptedPass, _ -> Some(Credentials(userName, ConfigFile.DecryptNuget encryptedPass)) - | Some userName, _, Some clearTextPass -> Some(Credentials(userName,clearTextPass)) + | Some userName, Some encryptedPass, _ -> Some(Credentials(userName, ConfigFile.DecryptNuget encryptedPass, AuthType.Basic)) + | Some userName, _, Some clearTextPass -> Some(Credentials(userName,clearTextPass, AuthType.Basic)) | _ -> None configNode diff --git a/src/Paket.Core/PublicAPI.fs b/src/Paket.Core/PublicAPI.fs index b1a2edd3f7..9498af1808 100644 --- a/src/Paket.Core/PublicAPI.fs +++ b/src/Paket.Core/PublicAPI.fs @@ -161,10 +161,10 @@ type Dependencies(dependenciesFileName: string) = projectName, installAfter)) /// Adds credentials for a Nuget feed - member this.AddCredentials(source: string, username: string, password : string) : unit = + member this.AddCredentials(source: string, username: string, password : string, authType : string) : unit = RunInLockedAccessMode( this.RootPath, - fun () -> ConfigFile.askAndAddAuth source username password |> returnOrFail ) + fun () -> ConfigFile.askAndAddAuth source username password authType |> returnOrFail ) /// Adds a token for a source member this.AddToken(source : string, token : string) : unit = diff --git a/src/Paket.Core/Versioning/ConfigFile.fs b/src/Paket.Core/Versioning/ConfigFile.fs index 2c2eea6e3e..894507e9fc 100644 --- a/src/Paket.Core/Versioning/ConfigFile.fs +++ b/src/Paket.Core/Versioning/ConfigFile.fs @@ -94,7 +94,7 @@ let private readPassword (message : string) : string = Console.Write "\b \b" password.Substring(0, (password.Length - 1)) else "" - else Console.Write "\r" + else Console.WriteLine() password let getAuthFromNode (node : XmlNode) = @@ -102,8 +102,13 @@ let getAuthFromNode (node : XmlNode) = | "credential" -> let username = node.Attributes.["username"].Value let password = node.Attributes.["password"].Value + let authType = + match node.Attributes.["authType"] with + | null -> AuthType.Basic + | n -> n.Value |> Utils.parseAuthTypeString + let salt = node.Attributes.["salt"].Value - Credentials (username, Decrypt salt password) + Credentials (username, Decrypt salt password, authType) | "token" -> Token node.Attributes.["value"].Value | _ -> failwith "unknown node" @@ -113,10 +118,11 @@ let private createSourceNode (credentialsNode : XmlNode) source nodeName = credentialsNode.AppendChild node |> ignore node -let private setCredentials (username : string) (password : string) (node : XmlElement) = +let private setCredentials (username : string) (password : string) (authType : string) (node : XmlElement) = let salt, encrypedPassword = Encrypt password node.SetAttribute ("username", username) node.SetAttribute ("password", encrypedPassword) + node.SetAttribute ("authType", authType) node.SetAttribute ("salt", salt) node @@ -167,7 +173,7 @@ let GetAuthenticationForUrl = let GetAuthentication (source : string) = GetAuthenticationForUrl(source,source) -let AddCredentials (source, username, password) = +let AddCredentials (source, username, password, authType) = trial { let! credentialsNode = getConfigNode "credentials" let newCredentials = @@ -175,11 +181,11 @@ let AddCredentials (source, username, password) = | None -> createSourceNode credentialsNode source "credential" |> Some | Some existingNode -> match getAuthFromNode existingNode with - | Credentials (_, existingPassword) -> + | Credentials (_, existingPassword, _) -> if existingPassword <> password then existingNode |> Some else None | _ -> None - |> Option.map (setCredentials username password) + |> Option.map (setCredentials username password authType) match newCredentials with | Some credentials -> do! saveConfigNode credentials | None -> () @@ -203,7 +209,7 @@ let AddToken (source, token) = | None -> () } -let askAndAddAuth (source : string) (username : string) (password : string) = +let askAndAddAuth (source : string) (username : string) (password : string) (authType : string) = let username = if username = "" then Console.Write "Username: " @@ -216,4 +222,11 @@ let askAndAddAuth (source : string) (username : string) (password : string) = readPassword "Password: " else password - AddCredentials (source.TrimEnd [|'/'|], username, password) + let authType = + if authType = "" then + Console.Write "Authentication type (basic|ntlm, default = basic): " + let input = Console.ReadLine().Trim() + if input = "" then "basic" else input + else + authType + AddCredentials (source.TrimEnd [|'/'|], username, password, authType) diff --git a/src/Paket.Core/Versioning/PackageSources.fs b/src/Paket.Core/Versioning/PackageSources.fs index 8d964dfe2d..c800a980e4 100644 --- a/src/Paket.Core/Versioning/PackageSources.fs +++ b/src/Paket.Core/Versioning/PackageSources.fs @@ -30,22 +30,24 @@ type EnvironmentVariable = [] type NugetSourceAuthentication = - | PlainTextAuthentication of username : string * password : string - | EnvVarAuthentication of usernameVar : EnvironmentVariable * passwordVar : EnvironmentVariable - | ConfigAuthentication of username : string * password : string + | PlainTextAuthentication of username : string * password : string * authType : Utils.AuthType + | EnvVarAuthentication of usernameVar : EnvironmentVariable * passwordVar : EnvironmentVariable * authType : Utils.AuthType + | ConfigAuthentication of username : string * password : string * authType : Utils.AuthType with override x.ToString() = match x with - | PlainTextAuthentication(u,_) -> sprintf "PlainTextAuthentication (username = %s, password = ***)" u - | EnvVarAuthentication(u,_) -> sprintf "EnvVarAuthentication (usernameVar = %s, passwordVar = ***)" u.Variable - | ConfigAuthentication(u,_) -> sprintf "ConfigAuthentication (username = %s, password = ***)" u + | PlainTextAuthentication(u,_,t) -> sprintf "PlainTextAuthentication (username = %s, password = ***, authType = %A)" u t + | EnvVarAuthentication(u,_,t) -> sprintf "EnvVarAuthentication (usernameVar = %s, passwordVar = ***, authType = %A)" u.Variable t + | ConfigAuthentication(u,_,t) -> sprintf "ConfigAuthentication (username = %s, password = ***, authType = %A)" u t member x.AsString = x.ToString() -let toBasicAuth = function - | PlainTextAuthentication(username,password) | ConfigAuthentication(username, password) -> - Credentials(username, password) - | EnvVarAuthentication(usernameVar, passwordVar) -> - Credentials(usernameVar.Value, passwordVar.Value) +let toCredentials = function + | PlainTextAuthentication(username,password,authType) -> + Credentials(username, password, authType) + | ConfigAuthentication(username, password,authType) -> + Credentials(username, password, authType) + | EnvVarAuthentication(usernameVar, passwordVar, authType) -> + Credentials(usernameVar.Value, passwordVar.Value, authType) let tryParseWindowsStyleNetworkPath (path : string) = let trimmed = path.TrimStart() @@ -77,7 +79,7 @@ type NugetSource = { Url : string Authentication : NugetSourceAuthentication option } member x.BasicAuth = - x.Authentication |> Option.map toBasicAuth + x.Authentication |> Option.map toCredentials type NugetV3SourceResourceJSON = { [] @@ -113,7 +115,7 @@ let getNuGetV3Resource (source : NugetV3Source) (resourceType : NugetV3ResourceT let key = source let getResourcesRaw () = async { - let basicAuth = source.Authentication |> Option.map toBasicAuth + let basicAuth = source.Authentication |> Option.map toCredentials let! rawData = safeGetFromUrl(basicAuth, source.Url, acceptJson) let rawData = match rawData with @@ -156,9 +158,10 @@ let getNuGetV3Resource (source : NugetV3Source) (resourceType : NugetV3ResourceT } let userNameRegex = Regex("username[:][ ]*[\"]([^\"]*)[\"]", RegexOptions.IgnoreCase ||| RegexOptions.Compiled) let passwordRegex = Regex("password[:][ ]*[\"]([^\"]*)[\"]", RegexOptions.IgnoreCase ||| RegexOptions.Compiled) +let authTypeRegex = Regex("authtype[:][ ]*[\"]([^\"]*)[\"]", RegexOptions.IgnoreCase ||| RegexOptions.Compiled) let internal parseAuth(text:string, source) = - let getAuth() = ConfigFile.GetAuthentication source |> Option.map (function Credentials(username, password) -> ConfigAuthentication(username, password) | _ -> ConfigAuthentication("","")) + let getAuth() = ConfigFile.GetAuthentication source |> Option.map (function Credentials(username, password, authType) -> ConfigAuthentication(username, password, authType) | _ -> ConfigAuthentication("","",AuthType.Basic)) if text.Contains("username:") || text.Contains("password:") then if not (userNameRegex.IsMatch(text) && passwordRegex.IsMatch(text)) then failwithf "Could not parse auth in \"%s\"" text @@ -166,16 +169,21 @@ let internal parseAuth(text:string, source) = let username = userNameRegex.Match(text).Groups.[1].Value let password = passwordRegex.Match(text).Groups.[1].Value + let authType = + if (authTypeRegex.IsMatch(text)) + then authTypeRegex.Match(text).Groups.[1].Value |> Utils.parseAuthTypeString + else Utils.AuthType.Basic + let auth = match EnvironmentVariable.Create(username), EnvironmentVariable.Create(password) with | Some userNameVar, Some passwordVar -> - EnvVarAuthentication(userNameVar, passwordVar) + EnvVarAuthentication(userNameVar, passwordVar, authType) | _, _ -> - PlainTextAuthentication(username, password) + PlainTextAuthentication(username, password, authType) - match toBasicAuth auth with - | Credentials(username, password) when username = "" && password = "" -> getAuth() + match toCredentials auth with + | Credentials(username, password, _) when username = "" && password = "" -> getAuth() | _ -> Some auth else getAuth() @@ -253,7 +261,7 @@ type PackageSource = static member WarnIfNoConnection (source,_) = let n url auth = - use client = Utils.createHttpClient(url, auth |> Option.map toBasicAuth) + use client = Utils.createHttpClient(url, auth |> Option.map toCredentials) try client.DownloadData url |> ignore with _ -> traceWarnfn "Unable to ping remote NuGet feed: %s." url diff --git a/src/Paket/Commands.fs b/src/Paket/Commands.fs index e7688894f5..470da354a9 100644 --- a/src/Paket/Commands.fs +++ b/src/Paket/Commands.fs @@ -63,6 +63,7 @@ type ConfigArgs = | [] AddToken of key_or_URL:string * token:string | [] Username of username:string | [] Password of password:string + | [] AuthType of authType:string with interface IArgParserTemplate with member this.Usage = @@ -71,6 +72,7 @@ with | AddToken(_) -> "add token for URL or credential key" | Username(_) -> "provide username" | Password(_) -> "provide password" + | AuthType (_) -> "specify authentication type: basic|ntlm (default: basic)" type ConvertFromNugetArgs = | [] Force diff --git a/src/Paket/Program.fs b/src/Paket/Program.fs index 36d8a40427..20cba96be0 100644 --- a/src/Paket/Program.fs +++ b/src/Paket/Program.fs @@ -223,8 +223,9 @@ let config (results : ParseResults<_>) = let args = results.GetResults <@ ConfigArgs.AddCredentials @> let source = args.Item 0 let username, password = results.GetResult (<@ ConfigArgs.Username @>, ""), results.GetResult (<@ ConfigArgs.Password @>, "") + let authType = results.GetResult (<@ ConfigArgs.AuthType @>, "") - Dependencies(".").AddCredentials(source, username, password) + Dependencies(".").AddCredentials(source, username, password, authType) | _, true -> let args = results.GetResults <@ ConfigArgs.AddToken @> let source, token = args.Item 0 diff --git a/tests/Paket.Tests/DependenciesFile/ParserSpecs.fs b/tests/Paket.Tests/DependenciesFile/ParserSpecs.fs index e0cb17865a..c78849bd32 100644 --- a/tests/Paket.Tests/DependenciesFile/ParserSpecs.fs +++ b/tests/Paket.Tests/DependenciesFile/ParserSpecs.fs @@ -680,23 +680,38 @@ let ``should read config without versions``() = cfg.GetDependenciesInGroup(Constants.MainDependencyGroup).[PackageName "FAKE"].Range |> shouldEqual (VersionRange.AtLeast "0") -let configWithPassword = """ -source http://www.nuget.org/api/v2 username: "tatü tata" password: "you got hacked!" +let configWithPasswordNoAuthType = """ +source http://www.nuget.org/api/v2 username: "tatü tata" password: "you got hacked!" nuget Rx-Main """ [] -let ``should read config with encapsulated password source``() = - let cfg = DependenciesFile.FromSource( configWithPassword) +let ``should read config with encapsulated password source with no auth type specified``() = + let cfg = DependenciesFile.FromSource(configWithPasswordNoAuthType) cfg.Groups.[Constants.MainDependencyGroup].Sources |> shouldEqual [ PackageSource.NuGetV2 { Url = "http://www.nuget.org/api/v2" - Authentication = Some (PlainTextAuthentication("tatü tata", "you got hacked!")) } ] + Authentication = Some (PlainTextAuthentication("tatü tata", "you got hacked!", Utils.AuthType.Basic)) } ] + +let configWithPasswordWithAuthType = """ +source http://www.nuget.org/api/v2 username: "tatü tata" password: "you got hacked!" authtype: "ntlm" +nuget Rx-Main +""" + +[] +let ``should read config with encapsulated password source and auth type specified``() = + let cfg = DependenciesFile.FromSource(configWithPasswordWithAuthType) + + cfg.Groups.[Constants.MainDependencyGroup].Sources + |> shouldEqual [ + PackageSource.NuGetV2 { + Url = "http://www.nuget.org/api/v2" + Authentication = Some (PlainTextAuthentication("tatü tata", "you got hacked!", Utils.AuthType.NTLM)) } ] let configWithPasswordInSingleQuotes = """ -source http://www.nuget.org/api/v2 username: 'tatü tata' password: 'you got hacked!' +source http://www.nuget.org/api/v2 username: 'tatü tata' password: 'you got hacked!' nuget Rx-Main """ @@ -725,7 +740,28 @@ let ``should read config with password in env variable``() = Url = "http://www.nuget.org/api/v2" Authentication = Some (EnvVarAuthentication ({Variable = "%FEED_USERNAME%"; Value = "user XYZ"}, - {Variable = "%FEED_PASSWORD%"; Value = "pw Love"}))} ] + {Variable = "%FEED_PASSWORD%"; Value = "pw Love"}, + Utils.AuthType.Basic))} ] + +let configWithPasswordInEnvVariableAndAuthType = """ +source http://www.nuget.org/api/v2 username: "%FEED_USERNAME%" password: "%FEED_PASSWORD%" authtype: "nTlM" +nuget Rx-Main +""" + +[] +let ``should read config with password in env variable and auth type specified``() = + Environment.SetEnvironmentVariable("FEED_USERNAME", "user XYZ", EnvironmentVariableTarget.Process) + Environment.SetEnvironmentVariable("FEED_PASSWORD", "pw Love", EnvironmentVariableTarget.Process) + let cfg = DependenciesFile.FromSource( configWithPasswordInEnvVariableAndAuthType) + + cfg.Groups.[Constants.MainDependencyGroup].Sources + |> shouldEqual [ + PackageSource.NuGetV2 { + Url = "http://www.nuget.org/api/v2" + Authentication = Some (EnvVarAuthentication + ({Variable = "%FEED_USERNAME%"; Value = "user XYZ"}, + {Variable = "%FEED_PASSWORD%"; Value = "pw Love"}, + Utils.AuthType.NTLM))} ] let configWithExplicitVersions = """ source "http://www.nuget.org/api/v2" diff --git a/tests/Paket.Tests/NuGetConfig/NuGetConfigSpecs.fs b/tests/Paket.Tests/NuGetConfig/NuGetConfigSpecs.fs index 9b788e9f0c..27dff7cc47 100644 --- a/tests/Paket.Tests/NuGetConfig/NuGetConfigSpecs.fs +++ b/tests/Paket.Tests/NuGetConfig/NuGetConfigSpecs.fs @@ -42,7 +42,7 @@ let ``can detect encrypted passwords in nuget.config``() = |> shouldEqual { PackageSources = [ "https://www.nuget.org/api/v2/", ("https://www.nuget.org/api/v2/",None) - "tc", ("https://tc/httpAuth/app/nuget/v1/FeedService.svc/", Some(Credentials("notty", "secret"))) ] + "tc", ("https://tc/httpAuth/app/nuget/v1/FeedService.svc/", Some(Credentials("notty", "secret", Utils.AuthType.Basic))) ] |> Map.ofList PackageRestoreEnabled = false PackageRestoreAutomatic = false } @@ -56,7 +56,7 @@ let ``can detect cleartextpasswords in nuget.config``() = |> shouldEqual { PackageSources = [ "https://www.nuget.org/api/v2/", ("https://www.nuget.org/api/v2/",None) - "somewhere", ("https://nuget/somewhere/",Some (Credentials("myUser", "myPassword"))) ] + "somewhere", ("https://nuget/somewhere/",Some (Credentials("myUser", "myPassword", Utils.AuthType.Basic))) ] |> Map.ofList PackageRestoreEnabled = false PackageRestoreAutomatic = false } diff --git a/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs b/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs index 22f56368fe..99c509a564 100644 --- a/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs +++ b/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs @@ -17,15 +17,31 @@ let sampleDoc() = doc [] -let ``get username and password from node``() = +let ``get username, password, and auth type from node``() = let doc = sampleDoc() let node = doc.CreateElement("credential") node.SetAttribute("username", "demo-user") let salt, password = Encrypt "demopassword" node.SetAttribute("password", password) node.SetAttribute("salt", salt) + node.SetAttribute("authType", "ntlm") // Act - let (Credentials(username, password)) = getAuthFromNode node + let (Credentials(username, password, Utils.AuthType.NTLM)) = getAuthFromNode node + + // Assert + username |> shouldEqual "demo-user" + password |> shouldEqual "demopassword" + +[] +let ``get username and password from node without auth type``() = + let doc = sampleDoc() + let node = doc.CreateElement("credential") + node.SetAttribute("username", "demo-user") + let salt, password = Encrypt "demopassword" + node.SetAttribute("password", password) + node.SetAttribute("salt", salt) + // Act + let (Credentials(username, password, Utils.AuthType.Basic)) = getAuthFromNode node // Assert username |> shouldEqual "demo-user"