Activity Pub Implementation #58
|
@ -150,7 +150,7 @@ func (s *ActivityPubService) GetActor(reqUrl string, fromGame string) (vocab.Act
|
|||
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||
|
||||
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/games/"+fromGame+"#main-key", nil, req)
|
||||
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/activitypub/actor#main-key", nil, req)
|
||||
if err != nil {
|
||||
slog.Error("Signing error", "err", err)
|
||||
return vocab.Actor{}, err
|
||||
|
@ -158,6 +158,7 @@ func (s *ActivityPubService) GetActor(reqUrl string, fromGame string) (vocab.Act
|
|||
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
slog.Error("failed to retrieve sender actor", "err", err, "url", reqUrl)
|
||||
return vocab.Actor{}, err
|
||||
}
|
||||
|
||||
|
@ -194,17 +195,20 @@ func (s *ActivityPubService) VerifySignature(r *http.Request, sender string) err
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
slog.Error("unable to retrieve actor for sig verification", "sender", sender)
|
||||
return err
|
||||
}
|
||||
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
|
||||
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
slog.Error("unable to decode pub key pem", "pubKeyPem", actor.PublicKey.PublicKeyPem)
|
||||
return err
|
||||
}
|
||||
slog.Info("retrieved pub key of sender", "actor", actor, "pubKey", pubKey)
|
||||
|
||||
verifier, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
slog.Error("invalid signature", "err", err)
|
||||
return err
|
||||
}
|
||||
return verifier.Verify(pubKey, httpsig.RSA_SHA256)
|
||||
|
|
|
@ -28,22 +28,22 @@ def actor_url(client):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def actor(client):
|
||||
def actor(client, actor_url):
|
||||
resp = client.get(actor_url, headers={"Content-Type": "application/activity+json"})
|
||||
assert resp.status_code == 200
|
||||
return resp.json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inbox(actor):
|
||||
def inbox_url(actor):
|
||||
return actor["inbox"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def outbox(actor):
|
||||
def outbox_url(actor):
|
||||
return actor["outbox"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def followers(actor):
|
||||
def followers_url(actor):
|
||||
return actor["followers"]
|
||||
|
|
|
@ -82,6 +82,7 @@ def webfinger():
|
|||
|
||||
@app.route("/users/h4kor")
|
||||
def actor():
|
||||
print("request to actor")
|
||||
return json.dumps(
|
||||
{
|
||||
"@context": [
|
||||
|
|
|
@ -1 +1,108 @@
|
|||
from datetime import datetime, timezone
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
ACCT_NAME = "acct:blog@localhost:3000"
|
||||
|
||||
PRIV_KEY_PEM = """-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCni8P4bvkC/3Sx
|
||||
NTrDw1qw0vWtJKZMsyJ3Mcs4+1apoVqOQhujUqGqFSiRT7Vmc7OEhB0vikdiTkCk
|
||||
1PcoTd/rOa/0WbG8385JcLzaJfTIG+rrRtHwZ1TwxwBju43jpGEZjpbA0dqoXMkr
|
||||
J1MyD7aPLoAiVe0ikw2czSZumv4ncemOtk0VG3b2TnIxo3CMKtUOWu8xT08MMIuo
|
||||
3cZRnpI6Xr/ULzvjv8e3EjIpwRJqMPECtGsfdcjFmR0yFIrjrlmkZTiW31z/Dk7i
|
||||
xRGD0ADy3WEQ3lA4l3mNZeyG4S0Wi4iYe9/wegESMZcakLoME7ks+KNS388Mdbcd
|
||||
DKy9NmWvAgMBAAECggEABLQAA0hHhdWv6+Lc9xkpFuTvxTV4fuyvCf4u1eGlnstg
|
||||
ZF/nW1/6w8XQ8WCgbJ4mKuZz1J14FYKxfoRaj8S9MA2Ff+wd+M77gRpAuDWajRzO
|
||||
LQk8OW2yd7POXKkAzvln9F9eofkCFKR4zSpPGTenCJaQkuYrQEOKfUf7oofdRzQi
|
||||
w9kmp3wAxM/EseHZpknYDCgDQV7MDQAaMD7kbynL2WfXPxebktwpRlKUwgtGrevj
|
||||
gagQL8J/GX6wO3ymw9sln4BhlI2+3LuiMXQdQc1tamkXFCguCuOZCu/2VRdCHmiS
|
||||
nnpu+FMspBHbvxO+RXo3Cu/S6jjJgoQxD2WZTE0gqQKBgQDM6AQdqBYjISdkI9Gl
|
||||
6ZLLjwZRJSYpopujtX7pun61l9kUwQevaR2Z39rMWxX62DD6arazi/ygIUBw6Kgp
|
||||
s/qBEb29ec+0cESdC8aJYb3dGvDzh/8C05p7ozxj8JZQcxq5W5jql/BELlSsUONO
|
||||
jfqQv8RGZNSkD9uy6TxOr4eWIwKBgQDRUuO/XRDLt8Mp10mTshxTznSQ3gAJYKeG
|
||||
0WfEC3kPEukHBQb8huqFcQDiQ71oBWuEdOQWgT3aBS6L+nIMyZMT5u+BejQm7/E5
|
||||
pMM+z0VRpfFSsIrCvU8yKam0aemQGlKQAfhTct1gCg+wKnYsSQMlNHKWEfDbw9I/
|
||||
cns/IN+dBQKBgQC6/Of0oFVDTZgC3GUPAO3C8QwUtM/0or1hUdk1Nck3shCZzeVT
|
||||
f5tRtmSWpHCUbwGTJBsCEjdBcda6srXzCJkLe8Moy6ZtxR34KqzM5fM7eMB1nJ9s
|
||||
Vunc9gPAN+cUF1ZF3H7ZZjoOHjGK5m3oW8xSl41np9Acv5P/2rP8Ilaa/QKBgQDJ
|
||||
YwISfitGk8mEW8hB/L4cMykapztJyl/i6Vz31EHoKr1fL4sFMZg4QfwjtCBqD6zd
|
||||
hshajoU/WHTr30wS2WxTXX9YBoZeX8KpPsdJioiagRioAYm+yfuDu2m2VZ+MMIb2
|
||||
Xa7YOk6Zs5RcXL3M5YHNLaSAlUoxZTjGKhJBLhN1MQKBgQCbo3ngBl7Qjjx4WJ93
|
||||
2WEEKvSDCv69eecNQDuKWKEiFqBN23LheNrN8DXMWFTtE4miY106dzQ0dUMh418x
|
||||
K98rXSX3VvY4w48AznvPMKVLqesFjcvwnBdvk/NqXod20CMSpOEVj6W/nGoTBQt2
|
||||
0PuW3IUym9KvO0WX9E+1Qw8mbw==
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
|
||||
def ensure_follow(client, inbox_url, actor_url):
|
||||
resp = client.post(
|
||||
inbox_url,
|
||||
json={
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"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
|
||||
|
||||
|
||||
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):
|
||||
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)
|
||||
body = json.dumps(data).encode()
|
||||
body_hash = hashlib.sha256(body).digest()
|
||||
digest = "SHA-256=" + base64.b64encode(body_hash).decode()
|
||||
date = datetime.now(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
host = "localhost:3000"
|
||||
target = urlparse(url).path
|
||||
to_sign = f"""(request-target): {method.lower()} {target}
|
||||
host: {host}
|
||||
date: {date}""".encode()
|
||||
sig = priv_key.sign(
|
||||
to_sign,
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256(),
|
||||
)
|
||||
sig_str = base64.b64encode(sig).decode()
|
||||
|
||||
request = requests.Request(method, url, data=body)
|
||||
request = request.prepare()
|
||||
request.headers["Content-Digest"] = digest
|
||||
request.headers["Host"] = host
|
||||
request.headers["Date"] = date
|
||||
request.headers["Signature"] = (
|
||||
f'keyId="http://mock_masto/users/h4kor#main-key",headers="(request-target) host date",signature="{sig_str}"'
|
||||
)
|
||||
return request
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from pprint import pprint
|
||||
import requests
|
||||
from .fixtures import ensure_follow, sign
|
||||
import pytest
|
||||
|
||||
|
||||
|
@ -22,3 +25,60 @@ def test_actor(client, actor_url):
|
|||
assert pubKey["owner"] == data["id"]
|
||||
assert pubKey["id"] != data["id"]
|
||||
assert "-----BEGIN RSA PUBLIC KEY-----" in pubKey["publicKeyPem"]
|
||||
|
||||
|
||||
def test_following(client, inbox_url, followers_url, actor_url):
|
||||
req = sign(
|
||||
"POST",
|
||||
inbox_url,
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://mock_masto/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
|
||||
"type": "Follow",
|
||||
"actor": "http://mock_masto:8000/users/h4kor",
|
||||
"object": actor_url,
|
||||
},
|
||||
)
|
||||
pprint(req.headers)
|
||||
resp = requests.Session().send(req)
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = client.get(
|
||||
followers_url, headers={"Content-Type": "application/activity+json"}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
pprint(data)
|
||||
assert "items" in data
|
||||
assert len(data["items"]) == 1
|
||||
|
||||
|
||||
# def test_unfollow(client, inbox_url, followers_url, actor_url):
|
||||
# ensure_follow(client, inbox_url, actor_url)
|
||||
|
||||
# resp = client.post(
|
||||
# inbox_url,
|
||||
# json={
|
||||
# "@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": "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
|
||||
|
|
|
@ -75,7 +75,7 @@ func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
|||
func (s *ActivityPubServer) Router(router fiber.Router) {
|
||||
router.Get("/actor", s.HandleActor)
|
||||
router.Get("/outbox", s.HandleOutbox)
|
||||
router.Get("/inbox", s.HandleInbox)
|
||||
router.Post("/inbox", s.HandleInbox)
|
||||
router.Get("/followers", s.HandleFollowers)
|
||||
}
|
||||
|
||||
|
@ -141,6 +141,7 @@ func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity)
|
|||
follower := act.Actor.GetID().String()
|
||||
err := s.apService.VerifySignature(r, follower)
|
||||
if err != nil {
|
||||
slog.Error("wrong signature", "err", err)
|
||||
return err
|
||||
}
|
||||
err = s.apService.AddFollower(follower)
|
||||
|
@ -153,7 +154,20 @@ func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *ActivityPubServer) processUndo(act *vocab.Activity) error {
|
||||
func (s *ActivityPubServer) processUndo(r *http.Request, act *vocab.Activity) error {
|
||||
follower := act.Actor.GetID().String()
|
||||
err := s.apService.VerifySignature(r, follower)
|
||||
if err != nil {
|
||||
slog.Error("wrong signature", "err", err)
|
||||
return err
|
||||
}
|
||||
err = s.apService.RemoveFollower(follower)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// go acpub.Accept(gameName, act)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -164,6 +178,7 @@ func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
|
|||
body := ctx.Request().Body()
|
||||
data, err := vocab.UnmarshalJSON(body)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse request body", "body", body, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -181,7 +196,7 @@ func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
|
|||
|
||||
if act.Type == vocab.UndoType {
|
||||
slog.Info("processing undo")
|
||||
return s.processUndo(act)
|
||||
return s.processUndo(r, act)
|
||||
}
|
||||
return errors.New("only follow and undo actions supported")
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue