sending accepts

This commit is contained in:
Niko Abeler 2024-05-16 21:11:02 +02:00
parent 0c8779def7
commit 3cbf952ae6
5 changed files with 193 additions and 118 deletions

View File

@ -1,10 +1,12 @@
package app package app
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
@ -17,6 +19,7 @@ import (
"time" "time"
vocab "github.com/go-ap/activitypub" vocab "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
) )
@ -42,7 +45,10 @@ func (cfg *ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc mode
func (cfg *ActivityPubConfig) PrivateKey() *rsa.PrivateKey { func (cfg *ActivityPubConfig) PrivateKey() *rsa.PrivateKey {
block, _ := pem.Decode([]byte(cfg.PrivateKeyPem)) block, _ := pem.Decode([]byte(cfg.PrivateKeyPem))
privKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
slog.Error("error x509.ParsePKCS1PrivateKey", "err", err)
}
return privKey return privKey
} }
@ -131,7 +137,13 @@ func (s *ActivityPubService) sign(privateKey *rsa.PrivateKey, pubKeyId string, b
return err return err
} }
func (s *ActivityPubService) GetActor(reqUrl string, fromGame string) (vocab.Actor, error) { func (s *ActivityPubService) GetActor(reqUrl string) (vocab.Actor, error) {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
c := http.Client{} c := http.Client{}
parsedUrl, err := url.Parse(reqUrl) parsedUrl, err := url.Parse(reqUrl)
@ -145,11 +157,6 @@ func (s *ActivityPubService) GetActor(reqUrl string, fromGame string) (vocab.Act
req.Header.Set("Date", time.Now().Format(http.TimeFormat)) req.Header.Set("Date", time.Now().Format(http.TimeFormat))
req.Header.Set("Host", parsedUrl.Host) req.Header.Set("Host", parsedUrl.Host)
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/activitypub/actor#main-key", nil, req) err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/activitypub/actor#main-key", nil, req)
if err != nil { if err != nil {
slog.Error("Signing error", "err", err) slog.Error("Signing error", "err", err)
@ -188,7 +195,9 @@ func (s *ActivityPubService) VerifySignature(r *http.Request, sender string) err
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig) s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig) s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
actor, err := s.GetActor(sender, siteConfig.FullUrl+"/activitypub/actor") slog.Info("verifying for", "sender", sender, "retriever", siteConfig.FullUrl+"/activitypub/actor")
actor, err := s.GetActor(sender)
// actor does not have a pub key -> don't verify // actor does not have a pub key -> don't verify
if actor.PublicKey.PublicKeyPem == "" { if actor.PublicKey.PublicKeyPem == "" {
return nil return nil
@ -213,3 +222,67 @@ func (s *ActivityPubService) VerifySignature(r *http.Request, sender string) err
} }
return verifier.Verify(pubKey, httpsig.RSA_SHA256) return verifier.Verify(pubKey, httpsig.RSA_SHA256)
} }
func (s *ActivityPubService) Accept(act *vocab.Activity) error {
actor, err := s.GetActor(act.Actor.GetID().String())
if err != nil {
return err
}
accept := vocab.AcceptNew(vocab.IRI("TODO"), act)
data, err := jsonld.WithContext(
jsonld.IRI(vocab.ActivityBaseURI),
).Marshal(accept)
if err != nil {
slog.Error("marshalling error", "err", err)
return err
}
return s.sendObject(actor, data)
}
func (s *ActivityPubService) sendObject(to vocab.Actor, data []byte) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
if to.Inbox == nil {
slog.Error("actor has no inbox", "actor", to)
return errors.New("actor has no inbox")
}
actorUrl, err := url.Parse(to.Inbox.GetID().String())
if err != nil {
slog.Error("parse error", "err", err)
return err
}
c := http.Client{}
req, _ := http.NewRequest("POST", to.Inbox.GetID().String(), bytes.NewReader(data))
req.Header.Set("Accept", "application/ld+json")
req.Header.Set("Date", time.Now().Format(http.TimeFormat))
req.Header.Set("Host", actorUrl.Host)
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/activitypub/actor#main-key", data, req)
if err != nil {
slog.Error("Signing error", "err", err)
return err
}
resp, err := c.Do(req)
if err != nil {
slog.Error("Sending error", "url", req.URL, "err", err)
return err
}
slog.Info("Request", "host", resp.Request.Header)
if resp.StatusCode > 299 {
body, _ := io.ReadAll(resp.Body)
slog.Error("Error sending Note", "method", resp.Request.Method, "url", resp.Request.URL, "status", resp.Status, "body", string(body))
return err
}
body, _ := io.ReadAll(resp.Body)
slog.Info("Sent Body", "body", string(data))
slog.Info("Retrieved", "status", resp.Status, "body", string(body))
return nil
}

View File

@ -52,23 +52,23 @@ def webfinger():
{ {
"subject": "acct:h4kor@mock_masto", "subject": "acct:h4kor@mock_masto",
"aliases": [ "aliases": [
"http://mock_masto/@h4kor", "http://mock_masto:8000/@h4kor",
"http://mock_masto/users/h4kor", "http://mock_masto:8000/users/h4kor",
], ],
"links": [ "links": [
{ {
"rel": "http://webfinger.net/rel/profile-page", "rel": "http://webfinger.net/rel/profile-page",
"type": "text/html", "type": "text/html",
"href": "http://mock_masto/@h4kor", "href": "http://mock_masto:8000/@h4kor",
}, },
{ {
"rel": "self", "rel": "self",
"type": "application/activity+json", "type": "application/activity+json",
"href": "http://mock_masto/users/h4kor", "href": "http://mock_masto:8000/users/h4kor",
}, },
{ {
"rel": "http://ostatus.org/schema/1.0/subscribe", "rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "http://mock_masto/authorize_interaction?uri={uri}", "template": "http://mock_masto:8000/authorize_interaction?uri={uri}",
}, },
{ {
"rel": "http://webfinger.net/rel/avatar", "rel": "http://webfinger.net/rel/avatar",
@ -120,53 +120,53 @@ def actor():
"focalPoint": {"@container": "@list", "@id": "toot:focalPoint"}, "focalPoint": {"@container": "@list", "@id": "toot:focalPoint"},
}, },
], ],
"id": "http://mock_masto/users/h4kor", "id": "http://mock_masto:8000/users/h4kor",
"type": "Person", "type": "Person",
"following": "http://mock_masto/users/h4kor/following", "following": "http://mock_masto:8000/users/h4kor/following",
"followers": "http://mock_masto/users/h4kor/followers", "followers": "http://mock_masto:8000/users/h4kor/followers",
"inbox": "http://mock_masto/users/h4kor/inbox", "inbox": "http://mock_masto:8000/users/h4kor/inbox",
"outbox": "http://mock_masto/users/h4kor/outbox", "outbox": "http://mock_masto:8000/users/h4kor/outbox",
"featured": "http://mock_masto/users/h4kor/collections/featured", "featured": "http://mock_masto:8000/users/h4kor/collections/featured",
"featuredTags": "http://mock_masto/users/h4kor/collections/tags", "featuredTags": "http://mock_masto:8000/users/h4kor/collections/tags",
"preferredUsername": "h4kor", "preferredUsername": "h4kor",
"name": "Niko", "name": "Niko",
"summary": '<p>Teaching computers to do things with arguable efficiency.</p><p>he/him</p><p><a href="http://mock_masto/tags/vegan" class="mention hashtag" rel="tag">#<span>vegan</span></a> <a href="http://mock_masto/tags/cooking" class="mention hashtag" rel="tag">#<span>cooking</span></a> <a href="http://mock_masto/tags/programming" class="mention hashtag" rel="tag">#<span>programming</span></a> <a href="http://mock_masto/tags/politics" class="mention hashtag" rel="tag">#<span>politics</span></a> <a href="http://mock_masto/tags/climate" class="mention hashtag" rel="tag">#<span>climate</span></a></p>', "summary": '<p>Teaching computers to do things with arguable efficiency.</p><p>he/him</p><p><a href="http://mock_masto:8000/tags/vegan" class="mention hashtag" rel="tag">#<span>vegan</span></a> <a href="http://mock_masto:8000/tags/cooking" class="mention hashtag" rel="tag">#<span>cooking</span></a> <a href="http://mock_masto:8000/tags/programming" class="mention hashtag" rel="tag">#<span>programming</span></a> <a href="http://mock_masto:8000/tags/politics" class="mention hashtag" rel="tag">#<span>politics</span></a> <a href="http://mock_masto:8000/tags/climate" class="mention hashtag" rel="tag">#<span>climate</span></a></p>',
"url": "http://mock_masto/@h4kor", "url": "http://mock_masto:8000/@h4kor",
"manuallyApprovesFollowers": False, "manuallyApprovesFollowers": False,
"discoverable": True, "discoverable": True,
"indexable": False, "indexable": False,
"published": "2018-08-16T00:00:00Z", "published": "2018-08-16T00:00:00Z",
"memorial": False, "memorial": False,
"devices": "http://mock_masto/users/h4kor/collections/devices", "devices": "http://mock_masto:8000/users/h4kor/collections/devices",
"publicKey": { "publicKey": {
"id": "http://mock_masto/users/h4kor#main-key", "id": "http://mock_masto:8000/users/h4kor#main-key",
"owner": "http://mock_masto/users/h4kor", "owner": "http://mock_masto:8000/users/h4kor",
"publicKeyPem": PUB_KEY_PEM, "publicKeyPem": PUB_KEY_PEM,
}, },
"tag": [ "tag": [
{ {
"type": "Hashtag", "type": "Hashtag",
"href": "http://mock_masto/tags/politics", "href": "http://mock_masto:8000/tags/politics",
"name": "#politics", "name": "#politics",
}, },
{ {
"type": "Hashtag", "type": "Hashtag",
"href": "http://mock_masto/tags/climate", "href": "http://mock_masto:8000/tags/climate",
"name": "#climate", "name": "#climate",
}, },
{ {
"type": "Hashtag", "type": "Hashtag",
"href": "http://mock_masto/tags/vegan", "href": "http://mock_masto:8000/tags/vegan",
"name": "#vegan", "name": "#vegan",
}, },
{ {
"type": "Hashtag", "type": "Hashtag",
"href": "http://mock_masto/tags/programming", "href": "http://mock_masto:8000/tags/programming",
"name": "#programming", "name": "#programming",
}, },
{ {
"type": "Hashtag", "type": "Hashtag",
"href": "http://mock_masto/tags/cooking", "href": "http://mock_masto:8000/tags/cooking",
"name": "#cooking", "name": "#cooking",
}, },
], ],
@ -188,7 +188,7 @@ def actor():
"value": '<a href="http://git.libove.org/h4kor/owl-blogs" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">git.libove.org/h4kor/owl-blogs</span><span class="invisible"></span></a>', "value": '<a href="http://git.libove.org/h4kor/owl-blogs" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">git.libove.org/h4kor/owl-blogs</span><span class="invisible"></span></a>',
}, },
], ],
"endpoints": {"sharedInbox": "http://mock_masto/inbox"}, "endpoints": {"sharedInbox": "http://mock_masto:8000/inbox"},
"icon": { "icon": {
"type": "Image", "type": "Image",
"mediaType": "image/png", "mediaType": "image/png",
@ -198,12 +198,17 @@ def actor():
) )
@app.route("/users/h4kor/inbox") @app.route("/users/h4kor/inbox", methods=["POST"])
def inbox(): def inbox():
if request.method == "POST": if request.method == "POST":
INBOX.append(request.get_json()) INBOX.append(json.loads(request.get_data()))
return "" return ""
@app.route("/msgs")
def msgs():
return json.dumps(INBOX)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port="8000") app.run(debug=True, host="0.0.0.0", port="8000")

View File

@ -1,7 +1,13 @@
from contextlib import contextmanager
from datetime import datetime, timezone from datetime import datetime, timezone
import json import json
from time import sleep
from urllib.parse import urlparse from urllib.parse import urlparse
import requests, base64, hashlib
from urllib.parse import urlparse
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
ACCT_NAME = "acct:blog@localhost:3000" ACCT_NAME = "acct:blog@localhost:3000"
@ -36,49 +42,23 @@ K98rXSX3VvY4w48AznvPMKVLqesFjcvwnBdvk/NqXod20CMSpOEVj6W/nGoTBQt2
def ensure_follow(client, inbox_url, actor_url): def ensure_follow(client, inbox_url, actor_url):
resp = client.post( req = sign(
"POST",
inbox_url, inbox_url,
json={ {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://mock_masto/d0b5768b-a15b-4ed6-bc84-84c7e2b57588", "id": "http://mock_masto:8000/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
"type": "Follow", "type": "Follow",
"actor": "http://mock_masto:8000/users/h4kor", "actor": "http://mock_masto:8000/users/h4kor",
"object": actor_url, "object": actor_url,
}, },
headers={"Content-Type": "application/activity+json"},
) )
resp = requests.Session().send(req)
assert resp.status_code == 200 assert resp.status_code == 200
def get_gmt_now() -> str:
return datetime.now(datetime.UTC).strftime("%a, %d %b %Y %H:%M:%S GMT")
from http_message_signatures import (
HTTPMessageSigner,
HTTPMessageVerifier,
HTTPSignatureKeyResolver,
algorithms,
)
import requests, base64, hashlib, http_sfv
class MyHTTPSignatureKeyResolver(HTTPSignatureKeyResolver):
keys = {"my-key": b"top-secret-key"}
def resolve_public_key(self, key_id: str):
return self.keys[key_id]
def resolve_private_key(self, key_id: str):
return priv_key
# return PRIV_KEY_PEM
def sign(method, url, data): def sign(method, url, data):
from urllib.parse import urlparse
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
priv_key = load_pem_private_key(PRIV_KEY_PEM.encode(), None) priv_key = load_pem_private_key(PRIV_KEY_PEM.encode(), None)
body = json.dumps(data).encode() body = json.dumps(data).encode()
@ -103,6 +83,20 @@ date: {date}""".encode()
request.headers["Host"] = host request.headers["Host"] = host
request.headers["Date"] = date request.headers["Date"] = date
request.headers["Signature"] = ( request.headers["Signature"] = (
f'keyId="http://mock_masto/users/h4kor#main-key",headers="(request-target) host date",signature="{sig_str}"' f'keyId="http://mock_masto:8000/users/h4kor#main-key",headers="(request-target) host date",signature="{sig_str}"'
) )
return request return request
@contextmanager
def msg_inc(n):
resp = requests.get("http://localhost:8000/msgs")
data = resp.json()
msgs = len(data)
yield
sleep(0.2)
resp = requests.get("http://localhost:8000/msgs")
data = resp.json()
assert msgs + n == len(
data
), f"prev: {msgs}, now: {len(data)}, expected: {msgs + n}"

View File

@ -1,6 +1,7 @@
from pprint import pprint from pprint import pprint
from time import sleep
import requests import requests
from .fixtures import ensure_follow, sign from .fixtures import ensure_follow, msg_inc, sign
import pytest import pytest
@ -28,18 +29,18 @@ def test_actor(client, actor_url):
def test_following(client, inbox_url, followers_url, actor_url): def test_following(client, inbox_url, followers_url, actor_url):
with msg_inc(1):
req = sign( req = sign(
"POST", "POST",
inbox_url, inbox_url,
{ {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://mock_masto/d0b5768b-a15b-4ed6-bc84-84c7e2b57588", "id": "http://mock_masto:8000/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
"type": "Follow", "type": "Follow",
"actor": "http://mock_masto:8000/users/h4kor", "actor": "http://mock_masto:8000/users/h4kor",
"object": actor_url, "object": actor_url,
}, },
) )
pprint(req.headers)
resp = requests.Session().send(req) resp = requests.Session().send(req)
assert resp.status_code == 200 assert resp.status_code == 200
@ -54,31 +55,33 @@ def test_following(client, inbox_url, followers_url, actor_url):
assert len(data["items"]) == 1 assert len(data["items"]) == 1
# def test_unfollow(client, inbox_url, followers_url, actor_url): def test_unfollow(client, inbox_url, followers_url, actor_url):
# ensure_follow(client, inbox_url, actor_url) ensure_follow(client, inbox_url, actor_url)
with msg_inc(1):
req = sign(
"POST",
inbox_url,
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://mock_masto:8000/users/h4kor#follows/3632040/undo",
"type": "Undo",
"actor": "http://mock_masto:8000/users/h4kor",
"object": {
"id": "http://mock_masto:8000/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
"type": "Follow",
"actor": "http://mock_masto:8000/users/h4kor",
"object": actor_url,
},
},
)
resp = requests.Session().send(req)
assert resp.status_code == 200
# resp = client.post( resp = client.get(
# inbox_url, followers_url, headers={"Content-Type": "application/activity+json"}
# json={ )
# "@context": "https://www.w3.org/ns/activitystreams", assert resp.status_code == 200
# "id": "http://mock_masto:8000/users/h4kor#follows/3632040/undo", data = resp.json()
# "type": "Undo", pprint(data)
# "actor": "http://mock_masto:8000/users/h4kor", assert "totalItems" in data
# "object": { assert data["totalItems"] == 0
# "id": "https://mock_masto/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
# "type": "Follow",
# "actor": "http://mock_masto:8000/users/h4kor",
# "object": actor_url,
# },
# },
# headers={"Content-Type": "application/activity+json"},
# )
# assert resp.status_code == 200
# resp = client.get(
# followers_url, headers={"Content-Type": "application/activity+json"}
# )
# assert resp.status_code == 200
# data = resp.json()
# assert "items" in data
# assert len(data["items"]) == 0

View File

@ -149,7 +149,7 @@ func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity)
return err return err
} }
// go acpub.Accept(gameName, act) go s.apService.Accept(act)
return nil return nil
} }