Skip to content

Commit eda2472

Browse files
CandleCandleSeanTAllen
authored andcommitted
Add the ability to change the hostname&service from a TCPConnectionNotify before connecting (#3230)
Added proxy_via to the tcp_connection_notify with a default implementation; used it in the tcp_connection constructors. Allows for proxies to be implemented by decorating a TCPConnectionNotify with another.
1 parent a1dbc36 commit eda2472

File tree

5 files changed

+151
-3
lines changed

5 files changed

+151
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to the Pony compiler and standard library will be documented
1212

1313
- Add `--link-ldcmd` command line argument for overriding the `ld` command used for linking ([PR #3259](https://github.com/ponylang/ponyc/pull/3259))
1414
- Make builds with `musl` on `glibc` systems possible ([PR #3263](https://github.com/ponylang/ponyc/pull/3263))
15+
- Add `proxy_via(destination_host, destination_service)` to `TCPConnectionNotify` to allow TCP handlers to change the hostname & service from a TCPConnectionNotify before connecting ([PR #3230](https://github.com/ponylang/ponyc/pull/3230))
1516

1617
### Changed
1718

packages/net/_test.pony

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ actor Main is TestList
1717
ifdef not windows then
1818
test(_TestTCPThrottle)
1919
end
20+
test(_TestTCPProxy)
2021

2122
class _TestPing is UDPNotify
2223
let _h: TestHelper
@@ -639,3 +640,32 @@ class _TestTCPThrottleSendNotify is TCPConnectionNotify
639640
conn.write("this is more data that you won't ever read" * 10000)
640641
end
641642
data
643+
644+
class _TestTCPProxy is UnitTest
645+
"""
646+
Check that the proxy callback is called on creation of a TCPConnection.
647+
"""
648+
fun name(): String => "net/TCPProxy"
649+
fun exclusion_group(): String => "network"
650+
651+
fun ref apply(h: TestHelper) =>
652+
h.expect_action("sender connected")
653+
h.expect_action("sender proxy request")
654+
655+
_TestTCP(h)(_TestTCPProxyNotify(h),
656+
_TestTCPProxyNotify(h))
657+
658+
class _TestTCPProxyNotify is TCPConnectionNotify
659+
let _h: TestHelper
660+
new iso create(h: TestHelper) =>
661+
_h = h
662+
663+
fun ref proxy_via(host: String, service: String): (String, String) =>
664+
_h.complete_action("sender proxy request")
665+
(host, service)
666+
667+
fun ref connected(conn: TCPConnection ref) =>
668+
_h.complete_action("sender connected")
669+
670+
fun ref connect_failed(conn: TCPConnection ref) =>
671+
_h.fail_action("sender connect failed")

packages/net/proxy.pony

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
interface Proxy
3+
fun apply(wrap: TCPConnectionNotify iso): TCPConnectionNotify iso^
4+
5+
class val NoProxy is Proxy
6+
"""
7+
Default implementation of a proxy that does not alter the supplied `TCPConnectionNotify`.
8+
9+
```pony
10+
actor MyClient
11+
new create(host: String, service: String, proxy: Proxy = NoProxy) =>
12+
let conn: TCPConnection = TCPConnection.create(
13+
env.root as AmbientAuth,
14+
proxy.apply(MyConnectionNotify.create()),
15+
"localhost",
16+
"80")
17+
```
18+
"""
19+
fun apply(wrap: TCPConnectionNotify iso): TCPConnectionNotify iso^ => wrap

packages/net/tcp_connection.pony

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,80 @@ actor TCPConnection
190190
these platforms, you **must** call `unmute` on a muted connection to have
191191
it close. Without calling `unmute` the `TCPConnection` actor will never
192192
exit.
193+
194+
## Proxy support
195+
196+
Using the `proxy_via` callback in a `TCPConnectionNotify` it is possible
197+
to implement proxies. The function takes the intended destination host
198+
and service as parameters and returns a 2-tuple of the proxy host and
199+
service.
200+
201+
The proxy `TCPConnectionNotify` should decorate another implementation of
202+
`TCPConnectionNotify` passing relevent data through.
203+
204+
### Example proxy implementation
205+
206+
```pony
207+
actor Main
208+
new create(env: Env) =>
209+
MyClient.create(
210+
"example.com", // we actually want to connect to this host
211+
"80",
212+
ExampleProxy.create("proxy.example.com", "80")) // we connect via this proxy
213+
214+
actor MyClient
215+
new create(host: String, service: String, proxy: Proxy = NoProxy) =>
216+
let conn: TCPConnection = TCPConnection.create(
217+
env.root as AmbientAuth,
218+
proxy.apply(MyConnectionNotify.create()),
219+
host,
220+
service)
221+
222+
class ExampleProxy is Proxy
223+
let _proxy_host: String
224+
let _proxy_service: String
225+
226+
new create(proxy_host: String, proxy_service: String) =>
227+
_proxy_host = proxy_host
228+
_proxy_service = proxy_service
229+
230+
fun apply(wrap: TCPConnectionNotify iso): TCPConnectionNotify iso^ =>
231+
ExampleProxyNotify.create(consume wrap, _proxy_service, _proxy_service)
232+
233+
class iso ExampleProxyNotify is TCPConnectionNotify
234+
// Fictional proxy implementation that has no error
235+
// conditions, and always forwards the connection.
236+
let _proxy_host: String
237+
let _proxy_service: String
238+
var _destination_host: (None | String) = None
239+
var _destination_service: (None | String) = None
240+
let _wrapped: TCPConnectionNotify iso
241+
242+
new iso create(wrap: TCPConnectionNotify iso, proxy_host: String, proxy_service: String) =>
243+
_wrapped = wrap
244+
_proxy_host = proxy_host
245+
_proxy_service = proxy_service
246+
247+
fun ref proxy_via(host: String, service: String): (String, String) =>
248+
// Stash the original host & service; return the host & service
249+
// for the proxy; indicating that the initial TCP connection should
250+
// be made to the proxy
251+
_destination_host = host
252+
_destination_service = service
253+
(_proxy_host, _proxy_service)
254+
255+
fun ref connected(conn: TCPConnection ref) =>
256+
// conn is the connection to the *proxy* server. We need to ask the
257+
// proxy server to forward this connection to our intended final
258+
// destination.
259+
conn.write((_destination_host + "\n").array())
260+
conn.write((_destination_service + "\n").array())
261+
wrapped.connected(conn)
262+
263+
fun ref received(conn, data, times) => _wrapped.received(conn, data, times)
264+
fun ref connect_failed(conn: TCPConnection ref) => None
265+
```
266+
193267
"""
194268
var _listen: (TCPListener | None) = None
195269
var _notify: TCPConnectionNotify
@@ -248,8 +322,9 @@ actor TCPConnection
248322
else
249323
AsioEvent.read_write()
250324
end
325+
(let host', let service') = _notify.proxy_via(host, service)
251326
_connect_count =
252-
@pony_os_connect_tcp[U32](this, host.cstring(), service.cstring(),
327+
@pony_os_connect_tcp[U32](this, host'.cstring(), service'.cstring(),
253328
from.cstring(), asio_flags)
254329
_notify_connecting()
255330

@@ -277,8 +352,9 @@ actor TCPConnection
277352
else
278353
AsioEvent.read_write()
279354
end
355+
(let host', let service') = _notify.proxy_via(host, service)
280356
_connect_count =
281-
@pony_os_connect_tcp4[U32](this, host.cstring(), service.cstring(),
357+
@pony_os_connect_tcp4[U32](this, host'.cstring(), service'.cstring(),
282358
from.cstring(), asio_flags)
283359
_notify_connecting()
284360

@@ -306,8 +382,9 @@ actor TCPConnection
306382
else
307383
AsioEvent.read_write()
308384
end
385+
(let host', let service') = _notify.proxy_via(host, service)
309386
_connect_count =
310-
@pony_os_connect_tcp6[U32](this, host.cstring(), service.cstring(),
387+
@pony_os_connect_tcp6[U32](this, host'.cstring(), service'.cstring(),
311388
from.cstring(), asio_flags)
312389
_notify_connecting()
313390

packages/net/tcp_connection_notify.pony

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,27 @@ interface TCPConnectionNotify
1111
"""
1212
None
1313

14+
fun ref proxy_via(host: String, service: String): (String, String) =>
15+
"""
16+
Called before before attempting to connect to the destination server
17+
In order to connect via proxy, return the host & service for the proxy
18+
server.
19+
20+
An implementation of this function might look like:
21+
```pony
22+
let _proxy_host = "some-proxy.example.com"
23+
let _proxy_service = "80"
24+
var _destination_host: ( None | String )
25+
var _destination_service: ( None | String )
26+
27+
fun ref proxy_via(host: String, service: String): (String, String) =>
28+
_destination_host = host
29+
_destination_service = service
30+
( _proxy_host, _proxy_service )
31+
```
32+
"""
33+
(host, service)
34+
1435
fun ref connecting(conn: TCPConnection ref, count: U32) =>
1536
"""
1637
Called if name resolution succeeded for a TCPConnection and we are now

0 commit comments

Comments
 (0)