Repository: mesonbuild/meson

Issue:
Support .netrc when fetching subprojects using wrap files 
**Describe the bug**
Currently, the only way to fetch urls with authentication is `source_url = ftp://user123:pass123@foo.com/some/file.zip`

However, critical information like passwords should be ideally hidden.

**To Reproduce**
Create a ~/.netrc with the following information:
```
machine foo.com
login user123
password pass123
```

Meson ignores the ~/.netrc file, and the following error appears when fetching the subproject:
```
<urlopen error ftp error: error_perm('530 Login incorrect.')>
WARNING: failed to download with error: could not get ftp://foo.com/some/file.zip is the internet available?. Trying after a delay...
```

**Expected behavior**
Defining a wrap file with `source_url = ftp://foo.com/some/file.zip` and fetch from it should not give any error, and should authenticate with data from the ~/.netrc file.


**system parameters**
```
$ meson --version
0.63.0
```

I write in representation of [Fluendo](https://fluendo.com). We want to implement it. Do you wish this feature?


Discussion:
For context, we currently use urllib from the python standard library, which doesn't automatically do this. See `mesonbuild/wrap/wrap.py`.

The need for authentication isn't the most commonly encountered occurrence, e.g. it's not needed for the WrapDB or more generally OSS projects... It's interesting that no one has mentioned it before now for proprietary wraps (especially as our documentation describes how to wrap prebuilt proprietary binaries). Although IIRC at least some of these people are using internally networked locations and don't need authentication.

> I write in representation of [Fluendo](https://fluendo.com/). We want to implement it. Do you wish this feature?

I have no objections to this feature.

It's great news that you're willing to implement it -- because it's not a priority for me at least to implement. 😉 I'll be happy to review it though.

Resolving patch:
diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py
--- a/mesonbuild/wrap/wrap.py
+++ b/mesonbuild/wrap/wrap.py
@@ -30,6 +30,8 @@
 import typing as T
 import textwrap
 
+from base64 import b64encode
+from netrc import netrc
 from pathlib import Path
 from . import WrapMode
 from .. import coredata
@@ -246,9 +248,19 @@ def __post_init__(self) -> None:
         self.subdir_root = os.path.join(self.source_dir, self.subdir)
         self.cachedir = os.path.join(self.subdir_root, 'packagecache')
         self.wraps = {} # type: T.Dict[str, PackageDefinition]
+        self.netrc: T.Optional[netrc] = None
         self.provided_deps = {} # type: T.Dict[str, PackageDefinition]
         self.provided_programs = {} # type: T.Dict[str, PackageDefinition]
         self.load_wraps()
+        self.load_netrc()
+
+    def load_netrc(self) -> None:
+        try:
+            self.netrc = netrc()
+        except FileNotFoundError:
+            return
+        except Exception as e:
+            mlog.warning(f'failed to process netrc file: {e}.', fatal=False)
 
     def load_wraps(self) -> None:
         if not os.path.isdir(self.subdir_root):
@@ -515,6 +527,16 @@ def get_svn(self) -> None:
         subprocess.check_call([svn, 'checkout', '-r', revno, self.wrap.get('url'),
                                self.directory], cwd=self.subdir_root)
 
+    def get_netrc_credentials(self, netloc: str) -> T.Optional[T.Tuple[str, str]]:
+        if self.netrc is None or netloc not in self.netrc.hosts:
+            return None
+
+        login, account, password = self.netrc.authenticators(netloc)
+        if account is not None:
+            login = account
+
+        return login, password
+
     def get_data(self, urlstring: str) -> T.Tuple[str, str]:
         blocksize = 10 * 1024
         h = hashlib.sha256()
@@ -525,8 +547,22 @@ def get_data(self, urlstring: str) -> T.Tuple[str, str]:
         elif WHITELIST_SUBDOMAIN in urlstring:
             raise WrapException(f'{urlstring} may be a WrapDB-impersonating URL')
         else:
+            headers = {'User-Agent': f'mesonbuild/{coredata.version}'}
+            creds = self.get_netrc_credentials(url.netloc)
+
+            if creds is not None and '@' not in url.netloc:
+                login, password = creds
+                if url.scheme == 'https':
+                    enc_creds = b64encode(f'{login}:{password}'.encode()).decode()
+                    headers.update({'Authorization': f'Basic {enc_creds}'})
+                elif url.scheme == 'ftp':
+                    urlstring = urllib.parse.urlunparse(url._replace(netloc=f'{login}:{password}@{url.netloc}'))
+                else:
+                    mlog.warning(f'Meson is not going to use netrc credentials for protocols other than https/ftp',
+                                  fatal=False)
+
             try:
-                req = urllib.request.Request(urlstring, headers={'User-Agent': f'mesonbuild/{coredata.version}'})
+                req = urllib.request.Request(urlstring, headers=headers)
                 resp = urllib.request.urlopen(req, timeout=REQ_TIMEOUT)
             except urllib.error.URLError as e:
                 mlog.log(str(e))


Test patch: