Compare commits

10 Commits
wip ... master

Author SHA1 Message Date
6f7e223b3d AP: move types to their own directory 2024-02-23 13:33:37 -03:00
b27e6cb2ec Better singleton names 2024-02-23 13:24:03 -03:00
810bf6b830 Improve ActivityStream objects
- Add optional fields
- Parse json correctly
- Add print helper
2024-02-22 20:58:36 -03:00
7fbdd031ff Minor improvements
- WebFinger:  detect when to use http or https, better function naming
- RequestPool: add user agent
2024-02-21 15:39:41 -03:00
166f428cd1 RP: use persistent per-thread Request object 2024-02-21 14:37:48 -03:00
9c5eb45d95 Better json management 2024-02-21 13:49:13 -03:00
f9dedca988 Initial AP objects implementation 2024-02-21 11:07:26 -03:00
a0b15f6526 DB: add SQLite support 2024-02-21 11:06:16 -03:00
58028ca68e Initial WebFinger
- Add headers to PRequest
2024-02-19 01:03:40 -03:00
b0847660cc RequestPool: initial implementation with TaskPool 2024-02-18 16:56:31 -03:00
15 changed files with 843 additions and 82 deletions

View File

@@ -8,4 +8,4 @@ dependency "handy-httpd" version="~>8.2.0"
dependency "ddbc" version="~>0.5.8" dependency "ddbc" version="~>0.5.8"
dependency "requests" version="~>2.1.3" dependency "requests" version="~>2.1.3"
dependency "hibernated" version="~>0.4.0" dependency "hibernated" version="~>0.4.0"
subConfiguration "ddbc" "PGSQL" dependency "openssl" version="~>3.3.3"

View File

@@ -1,7 +1,6 @@
{ {
"fileVersion": 1, "fileVersion": 1,
"versions": { "versions": {
"automem": "0.6.9",
"cachetools": "0.3.1", "cachetools": "0.3.1",
"d-unit": "0.10.2", "d-unit": "0.10.2",
"ddbc": "0.5.9", "ddbc": "0.5.9",
@@ -12,12 +11,11 @@
"httparsed": "1.2.1", "httparsed": "1.2.1",
"mysql-native": "3.1.0", "mysql-native": "3.1.0",
"odbc": "1.0.0", "odbc": "1.0.0",
"path-matcher": "1.1.3", "openssl": "3.3.3",
"path-matcher": "1.1.4",
"requests": "2.1.3", "requests": "2.1.3",
"slf4d": "3.0.0", "slf4d": "3.0.0",
"streams": "3.5.0", "streams": "3.5.0",
"test_allocator": "0.3.4", "undead": "1.1.8"
"undead": "1.1.8",
"unit-threaded": "0.10.8"
} }
} }

10
source/ap/errors.d Normal file
View File

@@ -0,0 +1,10 @@
module ap.errors;
/++
* Base exception for ActivityPub
+/
class APException : Exception {
this(string msg) {
super(msg);
}
}

74
source/ap/types/actor.d Normal file
View File

@@ -0,0 +1,74 @@
module ap.types.actor;
import std.json;
import ap.types.object;
import util;
/++
* Represents an ActivityPub Actor
* https://www.w3.org/TR/activitypub/#actors
+/
class Actor : ASObject {
/* Required fields */
string inbox; /// Link to messages received by this actor. Required
string outbox; /// Link to messages produced by this actor. Required
string following; /// Link to a collection of the actors that this actor is following. Required
string followers; /// Link to a collection of the actors that follow this actor. Required
/* Optional fields */
string liked; /// Link to a collection of objects this actor has liked
string streams; /// Supplementary list of Collections of interest
string preferredUsername; /// A short username for referencing this actor
ActorEndpoints endpoints; /// Useful endpoints for this actor
JSONValue raw;
/++
+ Creates an Actor object based on its json representation
+ Params:
+ json = ActivityPub actor JSON representation
+/
this(JSONValue json) {
super(json);
with (this) {
required(json, "inbox", inbox);
required(json, "outbox", outbox);
required(json, "following", following);
required(json, "followers", followers);
optional(json, "liked", liked);
optional(json, "streams", streams);
optional(json, "preferredUsername", preferredUsername);
endpoints = ActorEndpoints.fromJson(json.optional!JSONValue("endpoints"));
raw = json;
}
}
}
struct ActorEndpoints {
string proxyUrl;
string oauthAuthorizationEndpoint;
string oauthTokenEndpoint;
string provideClientKey;
string signClientKey;
string sharedInbox;
static ActorEndpoints fromJson(JSONValue json) {
ActorEndpoints endpoints = ActorEndpoints();
with (endpoints) {
optional(json, "proxyUrl", proxyUrl);
optional(json, "oauthAuthorizationEndpoint", oauthAuthorizationEndpoint);
optional(json, "oauthTokenEndpoint", oauthTokenEndpoint);
optional(json, "provideClientKey", provideClientKey);
optional(json, "signClientKey", signClientKey);
optional(json, "sharedInbox", sharedInbox);
}
return endpoints;
}
}

View File

@@ -0,0 +1,28 @@
module ap.types.collection;
import std.json;
import ap.types.object;
import ap.types.collection_page;
import util;
class Collection : ASObject {
int totalItems;
CollectionPage current;
CollectionPage first;
CollectionPage last;
ASObject items;
this(JSONValue json) {
super(json);
if (this.m_objType != ObjectType.Object)
throw new Exception("Wrong Collection format?");
optional(json, "totalItems", totalItems);
current = new CollectionPage(json.optional!JSONValue("current"));
first = new CollectionPage(json.optional!JSONValue("first"));
last = new CollectionPage(json.optional!JSONValue("last"));
items = new ASObject(json.optional!JSONValue("items"));
}
}

View File

@@ -0,0 +1,24 @@
module ap.types.collection_page;
import std.json;
import ap.types.collection;
import ap.types.object;
import util;
class CollectionPage : Collection {
CollectionPage partOf;
CollectionPage next;
CollectionPage prev;
this(JSONValue json) {
super(json);
if (this.m_objType != ObjectType.Object)
throw new Exception("Wrong CollectionPage format?");
partOf = new CollectionPage(json.optional!JSONValue("partOf"));
next = new CollectionPage(json.optional!JSONValue("next"));
prev = new CollectionPage(json.optional!JSONValue("prev"));
}
}

105
source/ap/types/link.d Normal file
View File

@@ -0,0 +1,105 @@
module ap.types.link;
import std.algorithm;
import std.array;
import std.format;
import std.json;
import ap.types.object;
import util;
class Link {
string type;
string href;
string[] rel;
string mediaType;
string name;
int height, width;
ASObject preview;
protected Store m_store;
protected ObjectType m_objType;
JSONValue raw;
this() {
}
this(JSONValue json) {
switch (json.type) {
case JSONType.ARRAY:
m_objType = ObjectType.Array;
m_store.links = json.array.map!(item => new Link(item)).array;
break;
case JSONType.OBJECT:
m_objType = ObjectType.Object;
optional(json, "type", type);
optional(json, "href", href);
optional(json, "mediaType", mediaType);
optional(json, "name", name);
optional(json, "height", height);
optional(json, "width", width);
const(JSONValue)* j;
if ((j = "rel" in json) != null)
rel = (*j).array.map!(item => item.str).array;
preview = new ASObject(json.optional!JSONValue("preview"));
break;
case JSONType.STRING:
m_objType = ObjectType.String;
m_store.str = json.str;
break;
case JSONType.NULL:
break;
default:
throw new Exception("Unrecognized ActivityStream Link JSON format");
}
this.raw = json;
}
string stringRep(int indentation = 0, string indentStr = " ") const {
string[] output;
string indent = "";
string j;
if (this.m_objType == ObjectType.String)
return format("%s%s", indent, this.m_store.str);
for (int i = 0; i < indentation; i++)
indent ~= indentStr;
output ~= format("%s {", this.type);
if (href)
output ~= format("%s%shref = %s", indent, indentStr, href);
if (mediaType)
output ~= format("%s%smediaType = %s", indent, indentStr, mediaType);
if (name)
output ~= format("%s%sname = %s", indent, indentStr, name);
if (height)
output ~= format("%s%sheight = %d", indent, indentStr, height);
if (width)
output ~= format("%s%swidth = %d", indent, indentStr, width);
if (preview && (j = preview.stringRep(indentation + 1)) != string.init)
output ~= format("%s%spreview = %s", indent, indentStr, j);
output ~= indent ~ "}";
if (output.length == 2)
return "";
return output.join("\n");
}
}

201
source/ap/types/object.d Normal file
View File

@@ -0,0 +1,201 @@
module ap.types.object;
import std.algorithm;
import std.array;
import std.datetime;
import std.format;
import std.json;
import ap.types.link;
import ap.types.collection;
import util;
union Store {
string str;
ASObject[] array;
Link[] links;
}
enum ObjectType {
Object,
String,
Array,
}
/++
* Basic ActivityStream Object
* https://www.w3.org/TR/activitypub/#obj
+/
class ASObject {
// Required fields (for root objects)
JSONValue context; /// Must be activitystream context
string id; /// AP requirement for unique identifier
string type; /// AP requirement for type of object
// Optional fields
ASObject attachment;
ASObject attributedTo;
ASObject audience;
string content;
string name;
DateTime endTime;
ASObject generator;
ASObject icon;
ASObject image;
ASObject inReplyTo;
ASObject location;
ASObject preview;
DateTime published;
Collection replies;
DateTime startTime;
string summary;
ASObject tag;
DateTime updated;
Link url;
ASObject to;
ASObject bto;
ASObject cc;
ASObject bcc;
string mediaType;
string duration;
JSONValue raw;
protected Store m_store;
protected ObjectType m_objType;
this() {
}
/++
+ Constructs the object according to json source
+ Params:
+ json = the source json
+ Returns: Object
+/
this(JSONValue json) {
switch (json.type) {
case JSONType.ARRAY:
this.m_objType = ObjectType.Array;
this.m_store.array = json.array.map!(item => new ASObject(item)).array;
break;
case JSONType.OBJECT:
this.m_objType = ObjectType.Object;
// String properties
optional(json, "context", context);
optional(json, "id", id);
optional(json, "type", type);
optional(json, "content", content);
optional(json, "name", name);
optional(json, "summary", summary);
optional(json, "mediaType", mediaType);
optional(json, "duration", duration);
// AS Object properties
generator = new ASObject(json.optional!JSONValue("generator"));
icon = new ASObject(json.optional!JSONValue("icon"));
image = new ASObject(json.optional!JSONValue("image"));
inReplyTo = new ASObject(json.optional!JSONValue("inReplyTo"));
location = new ASObject(json.optional!JSONValue("location"));
preview = new ASObject(json.optional!JSONValue("preview"));
tag = new ASObject(json.optional!JSONValue("tag"));
to = new ASObject(json.optional!JSONValue("to"));
bto = new ASObject(json.optional!JSONValue("bto"));
cc = new ASObject(json.optional!JSONValue("cc"));
bcc = new ASObject(json.optional!JSONValue("bcc"));
const(JSONValue)* j;
// Date properties
// if ((j = "endTime" in json) != null)
// endTime = DateTime.fromISOString(json["endTime"].str);
// if ((j = "published" in json) != null)
// published = DateTime.fromISOString(json["published"].str);
// if ((j = "startTime" in json) != null)
// startTime = DateTime.fromISOString(json["startTime"].str);
// if ((j = "updated" in json) != null)
// updated = DateTime.fromISOString(json["updated"].str);
url = new Link(json.optional!JSONValue("url"));
break;
case JSONType.STRING:
this.m_objType = ObjectType.String;
this.m_store.str = json.str;
break;
case JSONType.NULL:
break;
default:
throw new Exception("Unrecognized ActivityStream Object JSON format: " ~ json.type);
}
raw = json;
}
string stringRep(int indentation = 0, string indentStr = " ") const {
string[] output;
string indent = "";
string j;
if (this.m_objType == ObjectType.String)
return format("%s%s\n", indent, this.m_store.str);
if (this.m_objType == ObjectType.Array) {
output ~= "[";
foreach (obj; this.m_store.array)
output ~= format("%s%s%s%s,", indent, indentStr, indentStr, obj.stringRep(indentation + 1));
output ~= format("%s%s]", indent, indentStr);
return output.length == 2 ? "" : output.join("\n");
}
for (int i = 0; i < indentation; i++)
indent ~= indentStr;
output ~= format("%s {", this.type);
if (id)
output ~= format("%s%sid = %s", indent, indentStr, id);
if (content)
output ~= format("%s%scontent = %s", indent, indentStr, content);
if (name)
output ~= format("%s%sname = %s", indent, indentStr, name);
// if (summary)
// output ~= format("%s%ssummary = %s", indent, indentStr, summary);
if (mediaType)
output ~= format("%s%smediaType = %s", indent, indentStr, mediaType);
if (duration)
output ~= format("%s%sduration = %s", indent, indentStr, duration);
if (url && (j = url.stringRep(indentation + 1)) != string.init)
output ~= format("%s%surl = %s", indent, indentStr, j);
if (icon && (j = icon.stringRep(indentation + 1)) != string.init)
output ~= format("%s%sicon = %s", indent, indentStr, j);
if (image && (j = image.stringRep(indentation + 1)) != string.init)
output ~= format("%s%simage = %s", indent, indentStr, j);
if (tag && (j = tag.stringRep(indentation + 1)) != string.init)
output ~= format("%s%stag = %s", indent, indentStr, j);
output ~= indent ~ "}";
return output.length == 2 ? "" : output.join("\n");
}
}

77
source/ap/util.d Normal file
View File

@@ -0,0 +1,77 @@
module ap.util;
import std.format;
import std.json;
import requests;
import slf4d;
import singletons;
import net.request_pool;
import webfinger;
import ap.errors;
import ap.types.object;
enum {
ActivityJson = "application/activity+json",
ActivityJsonLd = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`,
}
/++
+ Fetches remote actor based on WebFinger link
+ Params:
+ wf = WebFinger link to user
+ Returns: User Actor JSON representation
+/
JSONValue apFetchRemoteUser(JSONValue wf) {
PRequest rq = PRequest();
rq.url = wf["href"].str;
rq.headers["Accept"] = wf["type"].str;
Response rs = RP.request(rq, true);
return parseJSON(cast(string) rs.responseBody);
}
/++
+ Fetches remote ActivityStream object by its id
+ Params:
+ id = the id of ActivityStream object
+ Returns: ActivityStream object
+/
ASObject apFetchASObject(string id, string contentType = ActivityJson) {
PRequest rq = PRequest();
rq.url = id;
rq.headers["Accept"] = contentType;
Response rs = RP.request(rq, true);
return new ASObject(parseJSON(cast(string) rs.responseBody));
}
/++
+ Resolves remote user to Actor based on username handle
+ Params:
+ username = Account username
+ Returns: ActivityPub Actor
+/
ASObject apResolveRemoteUsername(string username) {
JSONValue resource = wfRequestAccount(username);
JSONValue accountLink;
foreach (link; resource["links"].array)
if (
link["type"].str == ActivityJsonLd ||
link["type"].str == ActivityJson
) {
accountLink = link;
break;
}
if (accountLink == JSONValue())
throw new Exception("No valid activity streams href found for " ~ username);
string id = accountLink["href"].str;
string contentType = accountLink["type"].str;
// return new Actor(apFetchASObject(id, contentType));
return apFetchASObject(id, contentType);
}

View File

@@ -1,15 +1,18 @@
module db.db; module db.db;
import std.json;
import slf4d; import slf4d;
import ddbc.core; import ddbc.core;
import hibernated.core; import hibernated.core;
import db.types; import db.types;
enum DBConnector { enum DBConnector {
DB_MYSQL, Mysql,
DB_PGSQL, Postgres,
DB_SQLITE, Sqlite,
} }
struct DBSettings { struct DBSettings {
@@ -19,9 +22,42 @@ struct DBSettings {
string username; string username;
string password; string password;
string dbname; string dbname;
static DBSettings fromJson(JSONValue cfg) {
DBSettings settings;
with (settings) {
dbname = cfg["dbName"].str;
switch (cfg["connector"].str) {
case "postgresql":
connector = DBConnector.Postgres;
break;
case "sqlite":
connector = DBConnector.Sqlite;
break;
default:
throw new Exception("Database connector `" ~ cfg["connector"].str ~ "` not supported");
} }
class DB { if ("host" in cfg)
host = cfg["host"].str;
if ("port" in cfg)
port = cast(ushort) cfg["port"].integer;
if ("username" in cfg)
username = cfg["username"].str;
if ("password" in cfg)
username = cfg["password"].str;
}
return settings;
}
}
class Database {
protected DBSettings m_settings; protected DBSettings m_settings;
protected EntityMetaData m_schema; protected EntityMetaData m_schema;
protected DataSource m_ds; protected DataSource m_ds;
@@ -40,7 +76,7 @@ class DB {
Dialect dialect; Dialect dialect;
switch (this.m_settings.connector) { switch (this.m_settings.connector) {
case DBConnector.DB_PGSQL: case DBConnector.Postgres:
debugF!"Using PGSQL driver"; debugF!"Using PGSQL driver";
import ddbc.drivers.pgsqlddbc; import ddbc.drivers.pgsqlddbc;
@@ -55,6 +91,22 @@ class DB {
dialect = new PGSQLDialect(); dialect = new PGSQLDialect();
break; break;
case DBConnector.Sqlite:
debugF!"Using SQLite driver";
import ddbc.drivers.sqliteddbc;
driver = new SQLITEDriver();
url = this.m_settings.dbname;
static import std.file;
if (std.file.exists(url))
std.file.remove(url);
dialect = new SQLiteDialect();
break;
default: default:
throw new Exception("Database connector not supported (yet)"); throw new Exception("Database connector not supported (yet)");
} }

View File

@@ -1,48 +1,116 @@
module main; module main;
import std.json;
import requests;
import slf4d; import slf4d;
import slf4d.default_provider; import slf4d.default_provider;
import config; import config;
import singletons; import singletons;
import db.db; import db.db;
import net.request_pool;
import webfinger;
import ap.actor;
int main() { // import ap.util;
import std.math.remainder;
import util;
import ap.activity_stream;
import ap.util;
void commonInit() {
auto provider = new DefaultProvider(true, Levels.DEBUG); auto provider = new DefaultProvider(true, Levels.DEBUG);
configureLoggingProvider(provider); configureLoggingProvider(provider);
Config cfg = loadConfig();
initRequestPool(cfg);
initDatabase(cfg);
}
Config loadConfig() {
Config cfg = new Config(); Config cfg = new Config();
try {
cfg.load(); cfg.load();
} catch (Exception e) { return cfg;
error(e);
return 21;
} }
DBSettings dbSettings; void initRequestPool(Config cfg) {
auto dbCfg = cfg.v["db"]; RP = new RequestPool();
with (dbSettings) { RP.startBackground();
host = dbCfg["host"].str;
port = cast(ushort) dbCfg["port"].integer;
username = dbCfg["username"].str;
password = dbCfg["password"].str;
dbname = dbCfg["dbName"].str;
switch (dbCfg["connector"].str) {
case "postgresql":
connector = DBConnector.DB_PGSQL;
break;
default:
break;
}
} }
Db = new DB(dbSettings); void initDatabase(Config cfg) {
Db.connect(); DB = new Database(DBSettings.fromJson(cfg.v["db"]));
DB.connect();
scope (exit)
Db.close();
return 0;
} }
void main() {
import std.stdio;
commonInit();
scope (exit) {
DB.close();
RP.stop();
}
ASObject actor = apResolveRemoteUsername("@marisa@ak.gensokyo.shop");
// writeln(actor.raw.toJSON(true));
writeln(actor.stringRep);
// Actor marisa = apResolveRemoteUsername("@admin@localhost:8080");
// infoF!"Actor type: %s"(marisa.type);
// infoF!"Actor preferredUsername: %s"(marisa.preferredUsername);
// infoF!"Actor sharedInbox: %s"(marisa.endpoints.sharedInbox);
// PRequest followers = PRequest();
// with (followers) {
// url = marisa.followers;
// headers["Accept"] = "application/activity+json";
// }
// Response rs = RP.request(followers, true);
// infoF!"followers: %s"(rs.responseBody);
}
//int main() {
// auto provider = new DefaultProvider(true, Levels.DEBUG);
// configureLoggingProvider(provider);
//
// Config cfg = new Config();
//
// try {
// cfg.load();
// } catch (Exception e) {
// error(e);
// return 21;
// }
//
// DBSettings dbSettings;
// auto dbCfg = cfg.v["db"];
// with (dbSettings) {
// host = dbCfg["host"].str;
// port = cast(ushort) dbCfg["port"].integer;
// username = dbCfg["username"].str;
// password = dbCfg["password"].str;
// dbname = dbCfg["dbName"].str;
//
// switch (dbCfg["connector"].str) {
// case "postgresql":
// connector = DBConnector.DB_PGSQL;
// break;
// default:
// break;
// }
// }
//
// DB = new DB(dbSettings);
// DB.connect();
//
// scope (exit)
// DB.close();
//
// return 0;
//}
//

View File

@@ -1,67 +1,82 @@
module net.request_pool; module net.request_pool;
import core.thread;
import core.sync.semaphore;
import std.container; import std.container;
import std.uuid; import std.parallelism;
import requests; import requests;
import slf4d; import slf4d;
struct PRequest { struct PRequest {
string method = "GET";
string url; string url;
string[string] params; string method = "GET";
UUID uuid; /// Used internally QueryParam[] params;
string[string] headers;
string body = "";
string contentType = "text/plain";
} }
class RequestPool { class RequestPool {
private int m_totalWorkers; private int m_totalWorkers;
private ThreadGroup m_threads; private TaskPool m_taskPool;
private bool m_shouldRun = false; private Logger _l;
private Semaphore m_semaphore; private Request[] m_requests;
protected DList!PRequest m_requestsQueue;
this(int totalWorkers = 4) { this(int totalWorkers = 4) {
this.m_totalWorkers = totalWorkers; this.m_totalWorkers = totalWorkers;
this.m_semaphore = new Semaphore(); this._l = getLogger();
for (int i = 0; i < totalWorkers + 1; i++) {
Request rq = Request();
// TODO: add custom fields (such as user agent)
rq.addHeaders(["User-Agent": "apd (development)"]);
this.m_requests ~= rq;
}
} }
void startBackground() { void startBackground() {
debugF!"Starting RequestPool with %d workers"(this.m_totalWorkers); _l.debugF!"Starting RequestPool with %d workers"(this.m_totalWorkers);
this.m_shouldRun = true; this.m_taskPool = new TaskPool(this.m_totalWorkers);
for (int i = 0; i < this.m_totalWorkers; i++) {
Thread t = new Thread(() => this.m_run(i));
this.m_threads.add(t);
} }
} Response request(PRequest request, bool blocking = false) {
auto t = task(&this.m_run, request);
this.m_taskPool.put(t);
void enqueue(PRequest request) { if (blocking)
request.uuid = randomUUID(); return t.yieldForce();
this.m_requestsQueue.insertBack(request);
this.m_semaphore.notify(); return null;
} }
void stop() { void stop() {
this.m_shouldRun = false; this.m_taskPool.finish(true);
this.m_threads.joinAll();
} }
private void m_run(immutable int tid) { Response m_run(PRequest request) {
Request rq = Request(); Request rq = this.m_requests[this.m_taskPool.workerIndex];
rq.addHeaders(request.headers);
while (this.m_shouldRun) { _l.debugF!"[%d][%s] %s"(this.m_taskPool.workerIndex, request.method, request.url);
this.m_semaphore.wait();
PRequest request = this.m_requestsQueue.front();
this.m_requestsQueue.removeFront();
debugF!"Requesting %s with tid %d"(request.url, tid); Response rs;
Response rs = rq.execute(request.method, request.url); switch (request.method) {
} case "GET":
rs = rq.get(request.url, request.params);
break;
case "POST":
rs = rq.post(request.url, request.body, request.contentType);
break;
default:
errorF!"Unknown request method: %s"(request.method);
return null;
}
_l.debugF!"[%d][%s] %s result code: %d"(this.m_taskPool.workerIndex, request.method, request.url, rs
.code);
return rs;
} }
} }

View File

@@ -1,5 +1,7 @@
module singletons; module singletons;
import db.db; import db.db;
import net.request_pool;
DB Db; Database DB;
RequestPool RP;

46
source/util.d Normal file
View File

@@ -0,0 +1,46 @@
module util;
import std.format;
import std.json;
import slf4d;
import deimos.openssl.rsa;
import deimos.openssl.pem;
void optional(T)(ref JSONValue val, string key, ref T receiver) {
const(JSONValue)* p = key in val;
if (p == null)
return;
static if (is(JSONValue == T))
receiver = *p;
else
receiver = (*p).get!T;
}
T optional(T)(ref JSONValue val, string key) {
T t;
optional(val, key, t);
return t;
}
void required(T)(ref JSONValue val, string key, ref T receiver) {
receiver = val[key].get!T;
}
void generateRSA() {
RSA* rsa = RSA_generate_key(2048, 3, null, null);
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_RSAPrivateKey(bio, rsa, null, null, 0, null, null);
int keyLen = BIO_pending(bio);
char[] pemKey = new char[keyLen + 1];
BIO_read(bio, pemKey.ptr, keyLen);
BIO_free_all(bio);
RSA_free(rsa);
infoF!"private key: %s"(pemKey);
}

61
source/webfinger.d Normal file
View File

@@ -0,0 +1,61 @@
module webfinger;
import std.algorithm;
import std.array;
import std.format;
import std.json;
import std.typecons;
import requests;
import net.request_pool;
import singletons;
enum AcceptedWebfingerContentType = "application/jrd+json, application/json";
private bool checkValidWebfingerResponse(Response rs) {
return AcceptedWebfingerContentType.canFind(rs.responseHeaders()["content-type"].split(";")[0]);
}
private PRequest buildRequest(string uri, string[string] params) {
PRequest rq = PRequest();
string schema = "https";
if (uri.startsWith("localhost"))
schema = "http";
rq.method = "GET";
rq.url = format("%s://%s/.well-known/webfinger", schema, uri);
rq.headers["Accept"] = AcceptedWebfingerContentType;
QueryParam[] p;
foreach (k, v; params)
p ~= tuple!("key", "value")(k, v);
rq.params = p;
return rq;
}
PRequest buildAcctRequest(string uri, string acct) {
string[string] params = ["resource": format("acct:%s@%s", acct, uri)];
return buildRequest(uri, params);
}
JSONValue wfRequestAccount(string uri, string acct) {
Response rs = RP.request(buildAcctRequest(uri, acct), true);
if (checkValidWebfingerResponse(rs) == false)
throw new Exception("Invalid webfinger response");
return parseJSON(rs.responseBody().toString());
}
JSONValue wfRequestAccount(string handle) {
string[] uriAcct = handle.split("@")[$ - 2 .. $];
return wfRequestAccount(uriAcct[1], uriAcct[0]);
}