Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
**/*.o
**/*.lo
**/.libs
**/.deps
**/__pycache__
**/.pytest_cache
bin/*.log
bin/debug.log
bin/socket.log
bin/gnuworld.pid
lib/*.a
ddd
.vscode
28 changes: 19 additions & 9 deletions include/iServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ class iServer : public NetworkTarget {
* Flags that iServers may have.
*/
/* Set if this iServer is juped, false otherwise */
static const flagType FLAG_JUPE;
static constexpr flagType FLAG_JUPE = 0x01;
/* Set if this iServer is a hub (+h) */
static const flagType FLAG_HUB;
static constexpr flagType FLAG_HUB = 0x02;
/* Set if this iServer is a service (+s) */
static const flagType FLAG_SERVICE;
static constexpr flagType FLAG_SERVICE = 0x04;
/* Set if this iServer is IPv6-compatible (+6) */
static const flagType FLAG_IPV6;
static constexpr flagType FLAG_IPV6 = 0x08;
/* Set if this iServer is connected over TLS (+z) */
static constexpr flagType FLAG_TLS = 0x10;

/**
* Construct an iServer given its vital state variables
Expand All @@ -91,19 +93,17 @@ class iServer : public NetworkTarget {
/**
* Return true if a particular flag is set, false otherwise.
*/
inline bool getFlag(const flagType& whichFlag) const {
return ((flags & whichFlag) == whichFlag);
}
inline bool getFlag(flagType whichFlag) const { return ((flags & whichFlag) == whichFlag); }

/**
* Set a particular flags.
*/
inline void setFlag(const flagType& whichFlag) { flags |= whichFlag; }
inline void setFlag(flagType whichFlag) { flags |= whichFlag; }

/**
* Remove a particular flag.
*/
inline void removeFlag(const flagType& whichFlag) { flags &= ~whichFlag; }
inline void removeFlag(flagType whichFlag) { flags &= ~whichFlag; }

/**
* Return true if this server is a jupe.
Expand Down Expand Up @@ -145,6 +145,16 @@ class iServer : public NetworkTarget {
*/
inline void setIPv6() { setFlag(FLAG_IPV6); }

/**
* Return true if this server is connected over TLS
*/
inline bool isTLS() const { return getFlag(FLAG_TLS); }

/**
* Set this iServer as connected over TLS
*/
inline void setTLS() { setFlag(FLAG_TLS); }

/**
* Return the server numeric of this server's uplink.
*/
Expand Down
9 changes: 8 additions & 1 deletion include/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -788,8 +788,15 @@ class xServer : public ConnectionManager, public ConnectionHandler, public Netwo
* Set this server's uplink.
* This method should ONLY be called by the server command
* handlers.
* When our link to the uplink is TLS, mark the uplink iServer
* as TLS regardless of whether its SERVER line advertised +z.
*/
inline void setUplink(iServer* newUplink) { Uplink = newUplink; }
inline void setUplink(iServer* newUplink) {
Uplink = newUplink;
if (Uplink != nullptr && serverConnection != nullptr && serverConnection->isTLS()) {
Uplink->setTLS();
}
}

/**
* Enable or disable the burstBuffer.
Expand Down
19 changes: 18 additions & 1 deletion libgnuworld/StringTokenizer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ void StringTokenizer::Tokenize(const string& buf) {
if (respectQuotes && inQuotes) {
// \" inside a quoted run collapses to a literal ", staying
// in quote mode. Any other backslash is kept as-is.
if (('\\' == *currentPtr) && ((currentPtr + 1) != endPtr) && ('"' == *(currentPtr + 1))) {
if (('\\' == *currentPtr) && ((currentPtr + 1) != endPtr) &&
('"' == *(currentPtr + 1))) {
*addMePtr = '"';
++addMePtr;
++currentPtr;
Expand Down Expand Up @@ -211,6 +212,22 @@ string StringTokenizer::assemble(const size_type& start, int end) const {
return retMe;
}

/**
* trailing()
* Return the IRC trailing parameter: everything from the first
* token that begins with ':', with that leading ':' stripped.
* Returns std::nullopt if no such token exists.
*/
std::optional<string> StringTokenizer::trailing() const {
for (size_type i = 0; i < size(); ++i) {
if (!array[i].empty() && array[i][0] == ':') {
string t = assemble(i);
return t.substr(1);
}
}
return std::nullopt;
}

StringTokenizer::size_type StringTokenizer::totalChars() const {
size_type numChars = 0;

Expand Down
8 changes: 8 additions & 0 deletions libgnuworld/StringTokenizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include <vector>
#include <string>
#include <optional>
#include <iostream>

namespace gnuworld {
Expand Down Expand Up @@ -128,6 +129,13 @@ class StringTokenizer {
*/
std::string assemble(const size_type& = 0, int = -1) const;

/**
* Return the IRC trailing parameter: everything from the first
* token that begins with ':', with that leading ':' stripped.
* Returns std::nullopt if no such token exists.
*/
std::optional<std::string> trailing() const;

/**
* The immutable iterator type to use for walking through
* this object's tokens.
Expand Down
15 changes: 3 additions & 12 deletions mod.ccontrol/ccontrol.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6165,8 +6165,8 @@ bool ccontrol::glineChannelUsers(iClient* theClient, Channel* theChan, const str
// omitted - legacy senders; treat as no account (backwards compatible)
// * - explicitly no account
// <name> - logged-in account name
// Realname is located by the first ':'-prefixed token at index >= 5,
// so the optional account token does not shift a fixed fullname index.
// Realname is the IRC trailing parameter (st.trailing()), so the
// optional account token does not shift a fixed fullname index.
StringTokenizer st(Message);
if (st.size() < 6) {
return 0;
Expand All @@ -6188,16 +6188,7 @@ bool ccontrol::glineChannelUsers(iClient* theClient, Channel* theChan, const str
ipmask_parse(IP.c_str(), &theIP, NULL);
string base64IP = string(xIP(theIP).GetBase64IP());

size_t fullnameIdx = 5;
for (size_t i = 5; i < st.size(); ++i) {
if (!st[i].empty() && st[i][0] == ':') {
fullnameIdx = i;
break;
}
}
string fullname = st.assemble(fullnameIdx);
if (fullname.substr(0, 1) == ":")
fullname = fullname.substr(1);
string fullname = st.trailing().value_or(st.assemble(5));
fullname = theServer->getCharYY() + " " + fullname;

iClient* newClient = new (std::nothrow) iClient(
Expand Down
5 changes: 3 additions & 2 deletions mod.debug/SERVERINFOCommand.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ void dumpServerInfo(debug* bot, const iClient* theClient, iServer* theServer) {
prettyDuration(theServer->getStartTime()));
bot->Notice(theClient, "Lag: {} seconds (last update: {})", theServer->getLag(),
theServer->getLastLagTS());
bot->Notice(theClient, "Flags: {:#x} hub={} service={} ipv6={} jupe={} bursting={}",
bot->Notice(theClient, "Flags: {:#x} hub={} service={} ipv6={} tls={} jupe={} bursting={}",
theServer->getFlags(), theServer->isHub() ? "yes" : "no",
theServer->isService() ? "yes" : "no", theServer->isIPv6() ? "yes" : "no",
theServer->isJupe() ? "yes" : "no", theServer->isBursting() ? "yes" : "no");
theServer->isTLS() ? "yes" : "no", theServer->isJupe() ? "yes" : "no",
theServer->isBursting() ? "yes" : "no");
}

} // namespace
Expand Down
8 changes: 3 additions & 5 deletions src/iServer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ namespace gnuworld {

using std::string;

const iServer::flagType iServer::FLAG_JUPE = 0x01;
const iServer::flagType iServer::FLAG_HUB = 0x02;
const iServer::flagType iServer::FLAG_SERVICE = 0x04;
const iServer::flagType iServer::FLAG_IPV6 = 0x08;

iServer::iServer(const unsigned int& _uplink, const string& _yyxxx, const string& _name,
const time_t& _connectTime, const string& _description)
: NetworkTarget(_yyxxx), uplinkIntYY(_uplink), name(_name), connectTime(_connectTime),
Expand All @@ -64,6 +59,9 @@ void iServer::setFlags(const string& newFlags) {
case '6':
setIPv6();
break;
case 'z':
setTLS();
break;
case '+':
break;
default:
Expand Down
6 changes: 6 additions & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
test_stringtokenizer
test_stringtokenizer_trailing
test_signal
test_mtrie_perf_summary
test_mtrie_perf
Expand All @@ -10,3 +11,8 @@ test_gThread
test_econfig
test_burst
test_bot
test_xparameters_tags
harness/.venv/
harness/.pytest_cache/
harness/run/
harness/**/__pycache__/
8 changes: 7 additions & 1 deletion test/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ noinst_PROGRAMS = test_burst \
test_match \
test_bot \
test_kick_transaction \
test_xparameters_tags
test_xparameters_tags \
test_stringtokenizer_trailing

if COND_PCRE
noinst_PROGRAMS += test_regex
Expand Down Expand Up @@ -67,6 +68,11 @@ test_xparameters_tags_CXXFLAGS = -I${top_srcdir}/include \
-I${top_srcdir}/libgnuworld
test_xparameters_tags_LDADD = libgnuworld.la

test_stringtokenizer_trailing_SOURCES = test/stringtokenizer_trailing.cc
test_stringtokenizer_trailing_CXXFLAGS = -I${top_srcdir}/include \
-I${top_srcdir}/libgnuworld
test_stringtokenizer_trailing_LDADD = libgnuworld.la

test_regex_SOURCES = test/test_regex.cc
test_regex_LDADD = libgnuworld.la -lpcre

Expand Down
8 changes: 8 additions & 0 deletions test/harness/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.venv/
__pycache__/
*.pyc
.pytest_cache/
*.egg-info/
.coverage
htmlcov/
run/
65 changes: 65 additions & 0 deletions test/harness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# GNUWorld integration tests (`test/harness`)

Stand-alone pytest harness that:

1. Listens as a fake ircu P10 hub **on the host**
2. Runs **Postgres + gnuworld in Docker Compose**
3. Lets tests assert on S2S lines the hub receives and on **gnuworld container stdout**

Gnuworld is started with `docker compose run --rm -T` so its `-c` verbose
output is piped into the test process (`wait_for_stdout`).

C++ unit/load tools remain in `test/` (automake `test_*` binaries). This
directory is the Python integration suite only.

## Prerequisites

- Docker + Docker Compose
- A host build of gnuworld (`bin/gnuworld`, `lib/`, `share/gnuworld/`) — the
image copies these artifacts (it does not compile inside Docker)

## Setup

```bash
cd test/harness
python3 -m venv .venv
source .venv/bin/activate
pip install pytest pytest-asyncio pytest-timeout
```

## Run

```bash
cd test/harness
pytest -v
```

The session fixture builds the image, starts Postgres, and tears the stack
down at the end. First run is slower due to `docker compose build`.

## Writing tests

```python
async def test_example(linked):
hub, proc = linked
await proc.wait_for_stdout("Connected")
line = await hub.wait_for_token("EB")

async def test_check(ccontrol_linked):
hub, proc = ccontrol_linked
await hub.send_xquery(routing="iauth:1", message="CHECK ...")
```

## Layout

| Path | Role |
|------|------|
| `docker-compose.yml` | Postgres + gnuworld image |
| `docker/Dockerfile` | Runtime image from host-built binaries |
| `docker/initdb/` | ccontrol schema for Postgres |
| `fake_hub.py` | Listening fake P10 hub |
| `gnuworld_proc.py` | `compose run` + stdout capture |
| `data/*.conf.in` | Config templates |

Gnuworld reaches FakeHub at `127.0.0.1:<port>` (container uses host networking).
ccontrol reaches Postgres at `127.0.0.1:5433` (published compose port).
Loading