diff --git a/src/core/core.pri b/src/core/core.pri index 776f620b..afb66a4e 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -57,6 +57,8 @@ HEADERS += \ $$PWD/defercall.h \ $$PWD/socketnotifier.h \ $$PWD/eventloop.h \ + $$PWD/tcplistener.h \ + $$PWD/tcpstream.h \ $$PWD/logutil.h \ $$PWD/uuidutil.h \ $$PWD/zutil.h \ @@ -83,6 +85,8 @@ SOURCES += \ $$PWD/defercall.cpp \ $$PWD/socketnotifier.cpp \ $$PWD/eventloop.cpp \ + $$PWD/tcplistener.cpp \ + $$PWD/tcpstream.cpp \ $$PWD/logutil.cpp \ $$PWD/uuidutil.cpp \ $$PWD/zutil.cpp \ diff --git a/src/core/coretests.h b/src/core/coretests.h index 3c047169..2c411a76 100644 --- a/src/core/coretests.h +++ b/src/core/coretests.h @@ -4,6 +4,7 @@ int httpheaders_test(int argc, char **argv); int jwt_test(int argc, char **argv); int timer_test(int argc, char **argv); +int tcpstream_test(int argc, char **argv); int eventloop_test(int argc, char **argv); int defercall_test(int argc, char **argv); diff --git a/src/core/mod.rs b/src/core/mod.rs index ad1c9680..9620f0b3 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -125,6 +125,11 @@ mod tests { unsafe { call_c_main(ffi::defercall_test, args) as u8 } } + fn tcpstream_test(args: &[&OsStr]) -> u8 { + // SAFETY: safe to call + unsafe { call_c_main(ffi::tcpstream_test, args) as u8 } + } + fn eventloop_test(args: &[&OsStr]) -> u8 { // SAFETY: safe to call unsafe { call_c_main(ffi::eventloop_test, args) as u8 } @@ -150,6 +155,11 @@ mod tests { assert!(qtest::run(defercall_test)); } + #[test] + fn tcpstream() { + assert!(qtest::run(tcpstream_test)); + } + #[test] fn eventloop() { assert!(qtest::run(eventloop_test)); diff --git a/src/core/net.rs b/src/core/net.rs index 5b5780df..70c608f3 100644 --- a/src/core/net.rs +++ b/src/core/net.rs @@ -764,14 +764,15 @@ mod ffi { #[no_mangle] pub extern "C" fn tcp_listener_bind( - addr: *const c_char, + ip: *const c_char, + port: u16, out_errno: *mut c_int, ) -> *mut TcpListener { assert!(!out_errno.is_null()); - let addr = unsafe { CStr::from_ptr(addr) }; + let ip = unsafe { CStr::from_ptr(ip) }; - let addr = match addr.to_str() { + let ip = match ip.to_str() { Ok(s) => s, Err(_) => { unsafe { out_errno.write(libc::EINVAL) }; @@ -779,6 +780,16 @@ mod ffi { } }; + let ip: std::net::IpAddr = match ip.parse() { + Ok(ip) => ip, + Err(_) => { + unsafe { out_errno.write(libc::EINVAL) }; + return ptr::null_mut(); + } + }; + + let addr = std::net::SocketAddr::new(ip, port); + let l = match std::net::TcpListener::bind(addr) { Ok(l) => l, Err(e) => { @@ -805,6 +816,39 @@ mod ffi { } } + #[allow(clippy::missing_safety_doc)] + #[no_mangle] + pub unsafe extern "C" fn tcp_listener_local_addr( + l: *const TcpListener, + out_ip: *mut c_char, + out_ip_size: *mut libc::size_t, + out_port: *mut u16, + ) -> c_int { + let l = l.as_ref().unwrap(); + let out_ip_size = out_ip_size.as_mut().unwrap(); + assert!(!out_port.is_null()); + + let addr = match l.0.local_addr() { + Ok(addr) => addr, + Err(_) => return -1, + }; + + let ip = addr.ip().to_string(); + + if ip.len() > *out_ip_size { + // if value doesn't fit, return success with empty value + *out_ip_size = 0; + return 0; + } + + ptr::copy(ip.as_bytes().as_ptr() as *const c_char, out_ip, ip.len()); + *out_ip_size = ip.len(); + + out_port.write(addr.port()); + + 0 + } + #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn tcp_listener_as_raw_fd(l: *const TcpListener) -> c_int { diff --git a/src/core/tcplistener.cpp b/src/core/tcplistener.cpp new file mode 100644 index 00000000..72ad3ac8 --- /dev/null +++ b/src/core/tcplistener.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 Fastly, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tcplistener.h" + +#include "socketnotifier.h" + +TcpListener::TcpListener() : + inner_(nullptr) +{ +} + +TcpListener::~TcpListener() +{ + reset(); +} + +bool TcpListener::bind(const QHostAddress &addr, quint16 port) +{ + reset(); + + QByteArray ip = addr.toString().toUtf8(); + + int e; + inner_ = ffi::tcp_listener_bind(ip.data(), port, &e); + if(!inner_) + return false; + + int fd = ffi::tcp_listener_as_raw_fd(inner_); + + sn_ = std::make_unique(fd, SocketNotifier::Read); + sn_->activated.connect(boost::bind(&TcpListener::sn_activated, this)); + sn_->setEnabled(true); + + return true; +} + +std::tuple TcpListener::localAddress() const +{ + QByteArray ip(256, 0); + size_t ip_size = ip.size(); + quint16 port; + if(ffi::tcp_listener_local_addr(inner_, ip.data(), &ip_size, &port) != 0) + return {QHostAddress(), 0}; + + ip.resize(ip_size); + QHostAddress addr(QString::fromUtf8(ip)); + + return {addr, port}; +} + +std::unique_ptr TcpListener::accept() +{ + int e; + ffi::TcpStream *s_inner = ffi::tcp_listener_accept(inner_, &e); + if(!s_inner) + return std::unique_ptr(); // null + + TcpStream *s = new TcpStream; + s->inner_ = s_inner; + + return std::unique_ptr(s); +} + +void TcpListener::reset() +{ + sn_.reset(); + + ffi::tcp_listener_destroy(inner_); + inner_ = nullptr; +} + +void TcpListener::sn_activated() +{ + streamsReady(); +} diff --git a/src/core/tcplistener.h b/src/core/tcplistener.h new file mode 100644 index 00000000..5a649689 --- /dev/null +++ b/src/core/tcplistener.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 Fastly, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TCPLISTENER_H +#define TCPLISTENER_H + +#include +#include +#include +#include +#include "rust/bindings.h" +#include "tcpstream.h" + +class SocketNotifier; + +class TcpListener +{ +public: + TcpListener(); + ~TcpListener(); + + bool bind(const QHostAddress &addr, quint16 port); + std::tuple localAddress() const; + std::unique_ptr accept(); + + boost::signals2::signal streamsReady; + +private: + void reset(); + void sn_activated(); + + ffi::TcpListener *inner_; + std::unique_ptr sn_; +}; + +#endif diff --git a/src/core/tcpstream.cpp b/src/core/tcpstream.cpp new file mode 100644 index 00000000..83573de4 --- /dev/null +++ b/src/core/tcpstream.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 Fastly, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tcpstream.h" + +TcpStream::TcpStream() : + inner_(nullptr) +{ +} + +TcpStream::~TcpStream() +{ + ffi::tcp_stream_destroy(inner_); +} diff --git a/src/core/tcpstream.h b/src/core/tcpstream.h new file mode 100644 index 00000000..35ded3db --- /dev/null +++ b/src/core/tcpstream.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 Fastly, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TCPSTREAM_H +#define TCPSTREAM_H + +#include "rust/bindings.h" + +class TcpStream +{ +public: + ~TcpStream(); + +private: + friend class TcpListener; + + ffi::TcpStream *inner_; + + TcpStream(); +}; + +#endif diff --git a/src/core/tcpstreamtest.cpp b/src/core/tcpstreamtest.cpp new file mode 100644 index 00000000..595cf38b --- /dev/null +++ b/src/core/tcpstreamtest.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 Fastly, Inc. + * + * This file is part of Pushpin. + * + * $FANOUT_BEGIN_LICENSE:APACHE2$ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $FANOUT_END_LICENSE$ + */ + +#include +#include +#include +#include "timer.h" +#include "defercall.h" +#include "tcplistener.h" +#include "tcpstream.h" + +class TcpStreamTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + Timer::init(100); + } + + void cleanupTestCase() + { + DeferCall::cleanup(); + Timer::deinit(); + } + + void accept() + { + TcpListener l; + QVERIFY(l.bind(QHostAddress("127.0.0.1"), 0)); + + auto [addr, port] = l.localAddress(); + + bool streamsReady = false; + l.streamsReady.connect([&] { + streamsReady = true; + }); + + std::unique_ptr s = l.accept(); + QVERIFY(!s); + + QTcpSocket client; + client.connectToHost(addr, port); + + while(!streamsReady) + QTest::qWait(10); + + s = l.accept(); + QVERIFY(s); + + client.waitForConnected(-1); + + s.reset(); + } +}; + +namespace { +namespace Main { +QTEST_MAIN(TcpStreamTest) +} +} + +extern "C" { + +int tcpstream_test(int argc, char **argv) +{ + return Main::main(argc, argv); +} + +} + +#include "tcpstreamtest.moc" diff --git a/src/core/tests.pri b/src/core/tests.pri index e77d20d0..273dda87 100644 --- a/src/core/tests.pri +++ b/src/core/tests.pri @@ -6,4 +6,5 @@ SOURCES += \ $$PWD/jwttest.cpp \ $$PWD/timertest.cpp \ $$PWD/defercalltest.cpp \ + $$PWD/tcpstreamtest.cpp \ $$PWD/eventlooptest.cpp diff --git a/src/lib.rs b/src/lib.rs index 2e73513c..d820d64b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,6 +131,7 @@ pub mod ffi { pub fn jwt_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn timer_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn defercall_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; + pub fn tcpstream_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn eventloop_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn routesfile_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn proxyengine_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int;