In .NET core, Microsoft recently released DevTunnels which creates an outgoing connection to a centralized service which receives request from an external provided URL when subscribing to the service, sadly there is no equivalent solution for .NET Framework. As a quick fix I threw together a proxy that resolves this issue exposing the self-hosted debug session externally to any address you prefer. Assuming you code in user (non-admin) mode which is recommended, you may have to add the ACL to netsh to allow hosting at a particular IP/HOSTNAME.
The result of this post is that port 8080 will be forwarded to port 4858. Set port 4858 to the default port used by your IIS Debug session within visual studio.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Public Shared Async Sub LoadDebugProxy(args As String()) Dim IpAddressString As String Dim listenPortNum As Integer Dim forwardPortNum As Integer If args.Length < 3 OrElse Not Integer.TryParse(args(1), listenPortNum) OrElse Not Integer.TryParse(args(2), forwardPortNum) Then Debug.WriteLine("Usage: SimpleHttpProxy.exe <listenIP> <listenPort> <forwardPort>") Debug.WriteLine("Example: SimpleHttpProxy.exe 8080 5000") Return End If Dim server As New SimpleProxy(listenPortNum, forwardPortNum) Try IpAddressString = args(0) Debug.WriteLine($"Starting proxy server {listenPortNum}->{forwardPortNum}...") server.Start(IpAddressString).GetAwaiter().GetResult() ' Synchronously wait for Start to complete (which it won't, as it's an infinite loop) Catch ex As Exception Debug.WriteLine($"Proxy server failed to start or crashed: {ex.ToString()}") End Try Debug.WriteLine("Proxy server stopped.") End Sub Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) If Debugger.IsAttached() Then Dim MyLoadDebugProxyThread As New Threading.Thread(AddressOf LoadDebugProxy) 'This requires FULL site access due to TCP port! MyLoadDebugProxyThread.Start({"MyDNSNAme.noip.com", "8080", "4858"}) End If end sub |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
Imports System Imports System.IO Imports System.Linq Imports System.Net Imports System.Text Imports System.Threading.Tasks Public Class SimpleProxy Private ReadOnly _listenPort As Integer Private ReadOnly _forwardPort As Integer Private ReadOnly _forwardHost As String = "localhost" Public Sub New(listenPort As Integer, forwardPort As Integer) _listenPort = listenPort _forwardPort = forwardPort End Sub 'This is required or will receive access denied 'netsh http add urlacl url=http://{WANIP}:8080/ user=EVERYONE 'WanIP 'netsh http add urlacl url=http://192.168.0.XX:8080/ user=EVERYONE 'LANIP, 'netsh http add urlacl url=http://+:8080/ user=EVERYONE 'All IP's <-- requires admin during runtime 'netsh http add urlacl url=http://MyHostName.com:8080/ user=EVERYONE <-- DNS name Public Async Function Start(HostnameOrIP As String) As Task Dim listener As New HttpListener() Dim AddressToListenOn As String = $"http://{HostnameOrIP}:{_listenPort}/" listener.Prefixes.Add(AddressToListenOn) listener.Start() Debug.WriteLine($"Proxy listening on {AddressToListenOn}, forwarding to {_forwardHost}:{_forwardPort}") Do While True Try Dim context As HttpListenerContext = Await listener.GetContextAsync() ProcessRequest(context) ' Fire and forget Async Sub Catch ex As HttpListenerException If ex.ErrorCode = 995 Then Return ' Listener closed (occurs during shutdown) Debug.WriteLine($"Listener Error: {ex.Message}") Catch ex As Exception Debug.WriteLine($"Error accepting request: {ex.Message}") End Try Loop End Function Private Async Sub ProcessRequest(context As HttpListenerContext) Dim incomingRequest As HttpListenerRequest = context.Request Dim proxyResponse As HttpListenerResponse = context.Response Dim targetUrl As String = $"http://{_forwardHost}:{_forwardPort}{incomingRequest.RawUrl}" Dim forwardRequest As HttpWebRequest = DirectCast(WebRequest.Create(targetUrl), HttpWebRequest) Try forwardRequest.Method = incomingRequest.HttpMethod forwardRequest.ProtocolVersion = incomingRequest.ProtocolVersion forwardRequest.UserAgent = incomingRequest.UserAgent If incomingRequest.UrlReferrer IsNot Nothing Then forwardRequest.Referer = incomingRequest.UrlReferrer.AbsoluteUri End If forwardRequest.KeepAlive = incomingRequest.KeepAlive forwardRequest.ServicePoint.Expect100Continue = False For Each headerKey As String In incomingRequest.Headers.AllKeys Dim value As String = incomingRequest.Headers(headerKey) Try Select Case headerKey.ToLowerInvariant() Case "host" forwardRequest.Host = $"{_forwardHost}:{_forwardPort}" Case "connection" If value.ToLowerInvariant().Contains("keep-alive") Then forwardRequest.KeepAlive = True ElseIf value.ToLowerInvariant().Contains("close") Then forwardRequest.KeepAlive = False Else forwardRequest.Connection = value End If Case "content-length" forwardRequest.ContentLength = incomingRequest.ContentLength64 Case "content-type" forwardRequest.ContentType = incomingRequest.ContentType Case "accept" forwardRequest.Accept = value Case "expect" If String.Equals(value, "100-continue", StringComparison.OrdinalIgnoreCase) Then Continue For End If forwardRequest.Headers.Add(headerKey, value) Case "if-modified-since" Dim dt As DateTime If DateTime.TryParse(value, dt) Then forwardRequest.IfModifiedSince = dt End If Case "user-agent", "referer", "proxy-connection" ' User-Agent and Referer already set or handled by specific properties, Proxy-Connection is hop-by-hop Case Else forwardRequest.Headers.Add(headerKey, value) 'Range=bytes=0- End Select Catch ex As Exception ' Skip restricted headers (e.g., if trying to set a protected header) or invalid values End Try Next If incomingRequest.HttpMethod <> "GET" AndAlso incomingRequest.HttpMethod <> "HEAD" AndAlso incomingRequest.ContentLength64 > 0 Then Using requestStream As Stream = Await forwardRequest.GetRequestStreamAsync() Await incomingRequest.InputStream.CopyToAsync(requestStream) End Using ElseIf forwardRequest.Method = "POST" OrElse forwardRequest.Method = "PUT" Then If forwardRequest.ContentLength <= 0 AndAlso Not incomingRequest.Headers.AllKeys.Any(Function(k) k.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase)) Then forwardRequest.ContentLength = 0 End If End If Using targetResponse As HttpWebResponse = DirectCast(Await forwardRequest.GetResponseAsync(), HttpWebResponse) proxyResponse.StatusCode = CInt(targetResponse.StatusCode) proxyResponse.StatusDescription = targetResponse.StatusDescription proxyResponse.ProtocolVersion = targetResponse.ProtocolVersion Dim connHeaderValueTarget As String = targetResponse.Headers.Get("Connection") If connHeaderValueTarget IsNot Nothing Then proxyResponse.KeepAlive = connHeaderValueTarget.ToLowerInvariant().Contains("keep-alive") Else proxyResponse.KeepAlive = (targetResponse.ProtocolVersion >= HttpVersion.Version11) End If For i As Integer = 0 To targetResponse.Headers.Count - 1 Dim headerName As String = targetResponse.Headers.GetKey(i) Dim headerValue As String = targetResponse.Headers.Get(i) Select Case headerName.ToLowerInvariant() Case "content-length" proxyResponse.ContentLength64 = targetResponse.ContentLength Case "content-type" proxyResponse.ContentType = headerValue Case "transfer-encoding", "connection", "keep-alive" ' Transfer-Encoding is handled by CopyToAsync and HttpListener itself. ' Connection and Keep-Alive headers from target are used to set proxyResponse.KeepAlive property. Case Else Try proxyResponse.Headers.Add(headerName, headerValue) Catch ex As Exception ' Skip restricted headers End Try End Select Next Using responseStream As Stream = targetResponse.GetResponseStream() If responseStream IsNot Nothing Then Await responseStream.CopyToAsync(proxyResponse.OutputStream) End If End Using End Using Catch webEx As WebException Try If webEx.Response IsNot Nothing AndAlso TypeOf webEx.Response Is HttpWebResponse Then Dim errorResponse As HttpWebResponse = DirectCast(webEx.Response, HttpWebResponse) proxyResponse.StatusCode = CInt(errorResponse.StatusCode) proxyResponse.StatusDescription = errorResponse.StatusDescription proxyResponse.ProtocolVersion = errorResponse.ProtocolVersion For i As Integer = 0 To errorResponse.Headers.Count - 1 Dim headerName As String = errorResponse.Headers.GetKey(i) Dim headerValue As String = errorResponse.Headers.Get(i) Select Case headerName.ToLowerInvariant() Case "content-length" : proxyResponse.ContentLength64 = errorResponse.ContentLength Case "content-type" : proxyResponse.ContentType = headerValue Case Else Try : proxyResponse.Headers.Add(headerName, headerValue) Catch : End Try ' Skip restricted End Select Next If errorResponse.ContentLength > 0 Then Using errorStream As Stream = errorResponse.GetResponseStream() If errorStream IsNot Nothing Then ' Using synchronous Write in Catch block Dim buffer(4095) As Byte ' 4KB buffer Dim bytesRead As Integer Do bytesRead = errorStream.Read(buffer, 0, buffer.Length) If bytesRead > 0 Then proxyResponse.OutputStream.Write(buffer, 0, bytesRead) End If Loop While bytesRead > 0 End If End Using End If errorResponse.Close() Else proxyResponse.StatusCode = 502 ' Bad Gateway proxyResponse.StatusDescription = "Bad Gateway" Dim body As Byte() = Encoding.UTF8.GetBytes($"Proxy error (WebException without HTTP response): {webEx.Message}") proxyResponse.ContentType = "text/plain" proxyResponse.ContentLength64 = body.Length proxyResponse.OutputStream.Write(body, 0, body.Length) ' Synchronous Write End If Catch exInner As Exception ' If setting the error response itself fails (e.g., headers already sent) Debug.WriteLine($"Failed to send error response: {exInner.Message}") End Try Catch ex As Exception Try proxyResponse.StatusCode = 500 ' Internal Server Error proxyResponse.StatusDescription = "Internal Server Error" Dim body As Byte() = Encoding.UTF8.GetBytes($"Proxy error (General exception): {ex.Message}") proxyResponse.ContentType = "text/plain" proxyResponse.ContentLength64 = body.Length proxyResponse.OutputStream.Write(body, 0, body.Length) ' Synchronous Write Catch exInner As Exception Debug.WriteLine($"Failed to send 500 error response: {exInner.Message}") End Try Finally Try proxyResponse.Close() Catch finalEx As Exception Debug.WriteLine($"Error closing proxyResponse: {finalEx.Message}") End Try End Try End Sub End Class |