Repository: twisted/twisted

Issue:
WSGI applications run inside Twisted Web don’t get access to the client port
**Is your feature request related to a problem? Please describe.**
The problem we’re trying to solve is that WSGI applications run inside Twisted Web don’t get access to the client port (though the client address is made available through REMOTE_ADDR). We have an application in which the client’s port is necessary. 

When looking at the [`_WSGIResponse` class](https://github.com/twisted/twisted/blob/11c9f5808da4383678c9ff830d74137efd32d22d/src/twisted/web/wsgi.py#L292), we notice that only `REMOTE_ADDR` is set:
```
        # All keys and values need to be native strings, i.e. of type str in
        # *both* Python 2 and Python 3, so says PEP-3333.
        self.environ = {
            "REQUEST_METHOD": _wsgiString(request.method),
            "REMOTE_ADDR": _wsgiString(request.getClientAddress().host), <------
            "SCRIPT_NAME": _wsgiString(scriptName),
            "PATH_INFO": _wsgiString(pathInfo),
            "QUERY_STRING": _wsgiString(queryString),
            "CONTENT_TYPE": _wsgiString(request.getHeader(b"content-type") or ""),
            "CONTENT_LENGTH": _wsgiString(request.getHeader(b"content-length") or ""),
            "SERVER_NAME": _wsgiString(request.getRequestHostname()),
            "SERVER_PORT": _wsgiString(str(request.getHost().port)),
            "SERVER_PROTOCOL": _wsgiString(request.clientproto),
        }
```

**Describe the solution you'd like**
To provide support for retrieving the remote port as done with the remote IP address. This could be done by updating `environ` to include a new "REMOTE_PORT" attribute as following:
```
        self.environ = {
            "REQUEST_METHOD": _wsgiString(request.method),
            "REMOTE_ADDR": _wsgiString(request.getClientAddress().host), <------
            "REMOTE_PORT": _wsgiString(str(request.getClientAddress().port)),
            "SCRIPT_NAME": _wsgiString(scriptName),
```

**Describe alternatives you've considered**
Besides implementing a monkey patch, another alternative is to try and retrieve the port value from the WSGI context, which might have undesired side affects.

**Additional context**


Discussion:
Thanks Daniel for the report. 

Do you have time to send a PR request for this fix?
I will try to find time to review the change and have this fixed and merged.

I am not familiar with the WSGI standard, but I think that your solution makes sense.
I am familiar with the general low-level HTTP server api.
I can take a look at [PEP 3333](https://peps.python.org/pep-3333/) and do a review

Regards

Resolving patch:
diff --git a/src/twisted/web/wsgi.py b/src/twisted/web/wsgi.py
--- a/src/twisted/web/wsgi.py
+++ b/src/twisted/web/wsgi.py
@@ -287,9 +287,11 @@ def __init__(self, reactor, threadpool, application, request):
 
         # All keys and values need to be native strings, i.e. of type str in
         # *both* Python 2 and Python 3, so says PEP-3333.
+        remotePeer = request.getClientAddress()
         self.environ = {
             "REQUEST_METHOD": _wsgiString(request.method),
-            "REMOTE_ADDR": _wsgiString(request.getClientAddress().host),
+            "REMOTE_ADDR": _wsgiString(remotePeer.host),
+            "REMOTE_PORT": _wsgiString(str(remotePeer.port)),
             "SCRIPT_NAME": _wsgiString(scriptName),
             "PATH_INFO": _wsgiString(pathInfo),
             "QUERY_STRING": _wsgiString(queryString),
@@ -535,6 +537,9 @@ class WSGIResource:
     An L{IResource} implementation which delegates responsibility for all
     resources hierarchically inferior to it to a WSGI application.
 
+    The C{environ} argument passed to the application, includes the
+    C{REMOTE_PORT} key to complement the C{REMOTE_ADDR} key.
+
     @ivar _reactor: An L{IReactorThreads} provider which will be passed on to
         L{_WSGIResponse} to schedule calls in the I/O thread.
 


Test patch:
diff --git a/src/twisted/web/test/test_wsgi.py b/src/twisted/web/test/test_wsgi.py
--- a/src/twisted/web/test/test_wsgi.py
+++ b/src/twisted/web/test/test_wsgi.py
@@ -715,7 +715,11 @@ def test_remoteAddr(self):
         The C{'REMOTE_ADDR'} key of the C{environ} C{dict} passed to the
         application contains the address of the client making the request.
         """
-        d = self.render("GET", "1.1", [], [""])
+
+        def channelFactory():
+            return DummyChannel(peer=IPv4Address("TCP", "192.168.1.1", 12344))
+
+        d = self.render("GET", "1.1", [], [""], channelFactory=channelFactory)
         d.addCallback(self.environKeyEqual("REMOTE_ADDR", "192.168.1.1"))
 
         return d
@@ -735,6 +739,20 @@ def channelFactory():
 
         return d
 
+    def test_remotePort(self):
+        """
+        The C{'REMOTE_PORT'} key of the C{environ} C{dict} passed to the
+        application contains the port of the client making the request.
+        """
+
+        def channelFactory():
+            return DummyChannel(peer=IPv4Address("TCP", "192.168.1.1", 12344))
+
+        d = self.render("GET", "1.1", [], [""], channelFactory=channelFactory)
+        d.addCallback(self.environKeyEqual("REMOTE_PORT", "12344"))
+
+        return d
+
     def test_headers(self):
         """
         HTTP request headers are copied into the C{environ} C{dict} passed to