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
2 changes: 1 addition & 1 deletion .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
echo "filename=$filename" >> $GITHUB_ENV

- name: Upload strfry deb
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.filename }}
path: ${{ env.filename }}
Expand Down
101 changes: 96 additions & 5 deletions src/apps/relay/RelayIngester.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include "RelayServer.h"
#include "jsonParseUtils.h"
#include <cstdlib>


void RelayServer::runIngester(ThreadPool<MsgIngester>::Thread &thr) {
secp256k1_context *secpCtx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
Decompressor decomp;
flat_hash_map<uint64_t, AuthStatus*> connIdToAuthStatus;

while(1) {
auto newMsgs = thr.inbox.pop_all();
Expand All @@ -29,12 +32,20 @@ void RelayServer::runIngester(ThreadPool<MsgIngester>::Thread &thr) {
if (cfg().relay__logging__dumpInEvents) LI << "[" << msg->connId << "] dumpInEvent: " << msg->payload;

try {
ingesterProcessEvent(txn, msg->connId, msg->ipAddr, secpCtx, arr[1], writerMsgs);
ingesterProcessEvent(txn, msg->connId, connIdToAuthStatus, msg->ipAddr, secpCtx, arr[1], writerMsgs);
} catch (std::exception &e) {
sendOKResponse(msg->connId, arr[1].is_object() && arr[1].at("id").is_string() ? arr[1].at("id").get_string() : "?",
false, std::string("invalid: ") + e.what());
if (cfg().relay__logging__invalidEvents) LI << "Rejected invalid event: " << e.what();
}
} else if (cmd == "AUTH") {
if (cfg().relay__logging__dumpInAll) LI << "[" << msg->connId << "] dumpInAuth: " << msg->payload;

try {
ingesterProcessAuth(msg->connId, connIdToAuthStatus, secpCtx, arr[1]);
} catch (std::exception &e) {
sendNoticeError(msg->connId, std::string("auth failed: ") + e.what());
}
} else if (cmd == "REQ") {
if (cfg().relay__logging__dumpInReqs) LI << "[" << msg->connId << "] dumpInReq: " << msg->payload;

Expand Down Expand Up @@ -85,7 +96,7 @@ void RelayServer::runIngester(ThreadPool<MsgIngester>::Thread &thr) {
}
}

void RelayServer::ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output) {
void RelayServer::ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> &connIdToAuthStatus, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output) {
std::string packedStr, jsonStr;

parseAndVerifyEvent(origJson, secpCtx, true, true, packedStr, jsonStr);
Expand All @@ -104,9 +115,38 @@ void RelayServer::ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, std::str
});

if (foundProtected) {
LI << "Protected event, skipping";
sendOKResponse(connId, to_hex(packed.id()), false, "blocked: event marked as protected");
return;
// NIP-70 protected events must be rejected unless published by an authenticated public key
// that matches the event author, so we do all the AUTH flow here
if (cfg().relay__serviceUrl.empty()) {
// except if we don't have a serviceUrl, in that case just fail
LI << "Protected event and no serviceUrl configured, skipping";
sendOKResponse(connId, to_hex(packed.id()), false, "blocked: event marked as protected");
return;
}

auto as = connIdToAuthStatus.find(connId);
if (as == connIdToAuthStatus.end()) {
// we haven't sent an AUTH event for this, so first we generate a challenge for this connection
auto authStatus = new AuthStatus();
authStatus->challenge = std::to_string(int64_t(std::pow(packed.created_at(), connId + 1)));
connIdToAuthStatus.emplace(connId, authStatus);
LI << "Protected event, requesting AUTH";
sendAuthChallenge(connId, authStatus->challenge);
sendOKResponse(connId, to_hex(packed.id()), false, "auth-required: event marked as protected");
return;
}

const auto authed = (*as->second).authed;
if (authed.empty()) {
// not authenticated
sendOKResponse(connId, to_hex(packed.id()), false, "auth-required: event marked as protected");
return;
} else if (authed != packed.pubkey()) {
// authenticated as someone else
sendOKResponse(connId, to_hex(packed.id()), false, "restricted: must be published by the author");
return;
}
// otherwise we proceed to accept the event
}
}

Expand Down Expand Up @@ -137,6 +177,57 @@ void RelayServer::ingesterProcessClose(lmdb::txn &txn, uint64_t connId, const ta
tpReqWorker.dispatch(connId, MsgReqWorker{MsgReqWorker::RemoveSub{connId, SubId(jsonGetString(arr[1], "CLOSE subscription id was not a string"))}});
}

void RelayServer::ingesterProcessAuth(uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> connIdToAuthStatus, secp256k1_context *secpCtx, const tao::json::value &eventJson) {
if (cfg().relay__serviceUrl.empty()) {
throw herr("relay needs serviceUrl to be configured before AUTH can work");
}

std::string packedStr, jsonStr;
parseAndVerifyEvent(eventJson, secpCtx, true, true, packedStr, jsonStr);

PackedEventView packed(packedStr);

if (packed.kind() != 22242) {
throw herr("wrong event kind, expected 22242");
}

auto as = connIdToAuthStatus.find(connId);
if (as == connIdToAuthStatus.end()) {
throw herr("no auth status available for connection");
}
if (!(*as->second).authed.empty()) {
throw herr("already authenticated");
}
const auto challenge = (*as->second).challenge;

bool foundChallenge = false;
bool foundCorrectRelayUrl = false;

for (const auto &tagj : eventJson.at("tags").get_array()) {
const auto &tag = tagj.get_array();
if (tag.size() < 2) continue;
const auto name = tag[0].as<std::string_view>();
const auto value = tag[1].as<std::string_view>();
if (name == "relay" && value == cfg().relay__serviceUrl) {
foundCorrectRelayUrl = true;
} else if (name == "challenge" && value == challenge) {
foundChallenge = true;
}
}

if (!foundChallenge) {
throw herr("challenge string mismatch");
}
if (!foundCorrectRelayUrl) {
throw herr("incorrect or missing relay tag, expected: " + cfg().relay__serviceUrl);
}

// set the connection as authenticated with this pubkey
(*as->second).authed = packed.pubkey();

sendOKResponse(connId, to_hex(packed.id()), true, "successfully authenticated");
}

void RelayServer::ingesterProcessNegentropy(lmdb::txn &txn, Decompressor &decomp, uint64_t connId, const tao::json::value &arr) {
const auto &subscriptionStr = jsonGetString(arr[1], "NEG-OPEN subscription id was not a string");

Expand Down
15 changes: 14 additions & 1 deletion src/apps/relay/RelayServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ struct MsgNegentropy : NonCopyable {
MsgNegentropy(Var &&msg_) : msg(std::move(msg_)) {}
};

// NIP-42 stuff
struct AuthStatus {
std::string challenge;
std::string authed;
};


struct RelayServer {
uS::Async *hubTrigger = nullptr;
Expand All @@ -167,9 +173,10 @@ struct RelayServer {
void runWebsocket(ThreadPool<MsgWebsocket>::Thread &thr);

void runIngester(ThreadPool<MsgIngester>::Thread &thr);
void ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output);
void ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> &connIdToAuthStatus, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output);
void ingesterProcessReq(lmdb::txn &txn, uint64_t connId, const tao::json::value &origJson);
void ingesterProcessClose(lmdb::txn &txn, uint64_t connId, const tao::json::value &origJson);
void ingesterProcessAuth(uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> connIdToAuthStatus, secp256k1_context *secpCtx, const tao::json::value &eventJson);
void ingesterProcessNegentropy(lmdb::txn &txn, Decompressor &decomp, uint64_t connId, const tao::json::value &origJson);

void runWriter(ThreadPool<MsgWriter>::Thread &thr);
Expand Down Expand Up @@ -228,4 +235,10 @@ struct RelayServer {
tpWebsocket.dispatch(0, MsgWebsocket{MsgWebsocket::Send{connId, std::move(tao::json::to_string(reply))}});
hubTrigger->send();
}

void sendAuthChallenge(uint64_t connId, std::string_view challenge) {
auto reply = tao::json::value::array({ "AUTH", challenge });
tpWebsocket.dispatch(0, MsgWebsocket{MsgWebsocket::Send{connId, std::move(tao::json::to_string(reply))}});
hubTrigger->send();
}
};
4 changes: 4 additions & 0 deletions src/apps/relay/golpe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ config:
- name: relay__negentropy__maxSyncEvents
desc: "Maximum records that sync will process before returning an error"
default: 1000000

- name: relay__serviceUrl
desc: "Relay URL (beginning with wss://) that will be used to check NIP-42 AUTH"
default: ""
3 changes: 3 additions & 0 deletions strfry.conf
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,7 @@ relay {
# Maximum records that sync will process before returning an error
maxSyncEvents = 1000000
}

# NIP-42: URL that clients should use in the relay tag when authenticating (optional)
serviceUrl = "ws://localhost:7777"
}
Loading