AP stuff almost works
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Melody Becker 2025-04-09 17:35:31 +02:00
parent 98191fd098
commit d272fa90b4
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
20 changed files with 574 additions and 27 deletions

View file

@ -12,6 +12,7 @@ import (
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared"
@ -67,6 +68,7 @@ func notesFrom(w http.ResponseWriter, r *http.Request) {
user, err := dbgen.User.GetByUsername(username)
if err != nil {
log.Error().Err(err).Str("name", username).Msg("Failed to get user")
storage.HandleReconnectError(err)
httputils.HttpErr(w, 0, "failed to get user", http.StatusInternalServerError)
return
}

View file

@ -1,8 +1,9 @@
package webdebug
import (
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"database/sql"
"encoding/json"
"fmt"
@ -64,7 +65,10 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
return
}
publicKey, privateKey, err := ed25519.GenerateKey(nil)
// publicKey, privateKey, err := ed25519.GenerateKey(nil)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
pkeyId := make([]byte, 64)
_, err = rand.Read(pkeyId)
if err != nil {
@ -96,8 +100,8 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
Description: data.Description,
IsBot: data.IsBot,
ServerId: 1, // Hardcoded, Self is always first ID
PublicKey: publicKey,
PrivateKey: privateKey,
PublicKey: publicKeyBytes,
PrivateKey: privateKeyBytes,
PasskeyId: pkeyId,
}
if data.Birthday != nil {

View file

@ -0,0 +1,15 @@
package activitypub
import (
"fmt"
"net/http"
)
func BuildActivitypubRouter() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/user/{id}", users)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "in ap")
})
return router
}

View file

@ -0,0 +1,96 @@
package activitypub
import (
"encoding/json"
"fmt"
"net/http"
webutils "git.mstar.dev/mstar/goutils/http"
"github.com/rs/zerolog/log"
"git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
)
var baseLdContext = []any{
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
}
func users(w http.ResponseWriter, r *http.Request) {
type OutboundKey struct {
Id string `json:"id"`
Owner string `json:"owner"`
Pem string `json:"publicKeyPem"`
}
type Outbound struct {
Context []any `json:"@context"`
Id string `json:"id"`
Type string `json:"type"`
PreferredUsername string `json:"preferredUsername"`
Inbox string `json:"inbox"`
// FIXME: Public key stuff is borken. Focus on fixing
// PublicKey OutboundKey `json:"publicKey"`
}
userId := r.PathValue("id")
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).First()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get total user count from db")
}
return
}
// fmt.Println(x509.ParsePKCS1PublicKey(user.PublicKey))
apUrl := userIdToApUrl(user.ID)
data := Outbound{
Context: baseLdContext,
Id: apUrl,
Type: "Person",
PreferredUsername: user.DisplayName,
Inbox: apUrl + "/inbox",
// PublicKey: OutboundKey{
// Id: apUrl + "#main-key",
// Owner: apUrl,
// Pem: keyBytesToPem(user.PublicKey),
// },
}
encoded, err := json.Marshal(data)
w.Header().Add("Content-Type", "application/activity+json")
fmt.Fprint(w, string(encoded))
}
/*
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
Fuck you.
If anyone wants to get this shit working *the propper way* with JsonLD, here's the
original code
var chain goap.BaseApChain = &goap.EmptyBaseObject{}
chain = goap.AppendUDIdData(chain, apUrl)
chain = goap.AppendUDTypeData(chain, "Person")
chain = goap.AppendASPreferredNameData(chain, goap.ValueValue[string]{Value: user.DisplayName})
chain = goap.AppendW3SecurityPublicKeyData(
chain,
apUrl+"#main-key",
apUrl,
keyBytesToPem(user.PublicKey),
)
chainMap := chain.MarshalToMap()
proc := ld.NewJsonLdProcessor()
options := ld.NewJsonLdOptions("")
tmp, tmperr := json.Marshal(chainMap)
fmt.Println(string(tmp), tmperr)
data, err := proc.Compact(chainMap, baseLdContext, options)
// data, err := goap.Compact(chain, baseLdContext)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal ap chain")
webutils.ProblemDetailsStatusOnly(w, 500)
return
}
*/

View file

@ -0,0 +1,25 @@
package activitypub
import (
"encoding/pem"
"fmt"
"git.mstar.dev/mstar/linstrom/config"
)
func userIdToApUrl(id string) string {
return fmt.Sprintf(
"%s/api/ap/users/%s",
config.GlobalConfig.General.GetFullPublicUrl(),
id,
)
}
func keyBytesToPem(bytes []byte) string {
block := pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: bytes,
}
return string(pem.EncodeToMemory(&block))
}

20
web/public/api/api.go Normal file
View file

@ -0,0 +1,20 @@
package api
import (
"fmt"
"net/http"
"git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
)
func BuildApiRouter() http.Handler {
router := http.NewServeMux()
router.Handle(
"/activitypub/",
http.StripPrefix("/activitypub", activitypub.BuildActivitypubRouter()),
)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "in api")
})
return router
}

167
web/public/api/webfinger.go Normal file
View file

@ -0,0 +1,167 @@
package api
import (
"errors"
"fmt"
"net/http"
"regexp"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/other"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/shared"
"git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
)
var webfingerResourceRegex = regexp.MustCompile(`acct:(?P<username>[\w-]+)@(?<domain>[\w\.-]+)`)
func WellKnownWebfinger(w http.ResponseWriter, r *http.Request) {
type OutboundLink struct {
Relation string `json:"rel"`
Type string `json:"type"`
Href *string `json:"href,omitempty"`
}
type Outbound struct {
Subject string `json:"subject"`
Links []OutboundLink `json:"links"`
Aliases []string `json:"aliases,omitempty"`
}
requestedResource := r.FormValue("resource")
matches := webfingerResourceRegex.FindStringSubmatch(requestedResource)
if len(matches) == 0 {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/webfinger-bad-resource",
"Bad resource format",
other.IntoPointer(
`The "resource" parameter must be available and of the form "acct:<username>@<domain>`,
),
nil,
)
return
}
// NOTE: Safe to access since, if the regex matches, it must include both groups
// Index 0 is the full matching string
// Index 1 is the username
// Index 2 is something vaguely domain-like
username := matches[1]
domain := matches[2]
// Fail if requested user is a different domain
// TODO: Decide whether to include the info that it's a different domain
if domain != config.GlobalConfig.General.GetFullDomain() {
webutils.ProblemDetailsStatusOnly(w, 404)
return
}
user, err := dbgen.User.GetByUsername(username)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
webutils.ProblemDetailsStatusOnly(w, 404)
} else {
// Fail the request, then attempt to reconnect
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get user from db")
}
}
return
}
data := Outbound{
Subject: matches[0],
Links: []OutboundLink{
{
Relation: "self",
Type: "application/activity+json",
Href: other.IntoPointer(
fmt.Sprintf(
"%s/api/activitypub/user/%s",
config.GlobalConfig.General.GetFullPublicUrl(),
user.ID,
),
),
},
{
Relation: "http://webfinger.net/rel/profile-page",
Type: "text/html",
Href: other.IntoPointer(
fmt.Sprintf(
"%s/user/%s",
config.GlobalConfig.General.GetFullPublicUrl(),
user.ID,
),
),
},
},
}
webutils.SendJson(w, &data)
}
func Nodeinfo(w http.ResponseWriter, r *http.Request) {
u := dbgen.User
userCount, err := u.Where(u.DeletedAt.IsNull(), u.Verified.Is(true)).Count()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get total user count from db")
}
return
}
n := dbgen.Note
noteCount, err := n.Where(n.DeletedAt.IsNull(), n.OriginId.Eq(1)).Count()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get total user count from db")
}
return
}
data := map[string]any{
"version": "2.1",
"software": map[string]string{
"name": "linstrom",
"version": shared.Version,
"homepage": "https://git.mstar.dev/mstar/linstrom",
"repository": "https://git.mstar.dev/mstar/linstrom",
},
"protocols": []string{"activitypub"},
"services": map[string]any{
"inbound": []string{},
"outbound": []string{},
},
"openRegistrations": config.GlobalConfig.Admin.AllowRegistration,
"usage": map[string]any{
"users": map[string]any{
"total": userCount,
"activeHalfyear": nil,
"activeMonth": nil,
},
"localPosts": noteCount,
"localComments": 0,
},
"metadata": map[string]any{},
}
webutils.SendJson(w, data)
}
func WellKnownNodeinfo(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"links": []map[string]any{
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
"href": config.GlobalConfig.General.GetFullPublicUrl() + "/nodeinfo/2.1",
},
},
}
webutils.SendJson(w, data)
}

22
web/public/errorpages.go Normal file
View file

@ -0,0 +1,22 @@
package webpublic
import (
"fmt"
"net/http"
webutils "git.mstar.dev/mstar/goutils/http"
)
var errorDescriptions = map[string]string{
"webfinger-bad-resource": "The given format for the \"resource\" url parameter was missing or invalid. It must follow the form \"acct:<username>@<domain>\"",
"db-failure": "The database query for this request failed for an undisclosed reason. This is often caused by bad input data conflicting with existing information. Try to submit different data or wait for some time",
}
func errorTypeHandler(w http.ResponseWriter, r *http.Request) {
errName := r.PathValue("name")
if description, ok := errorDescriptions[errName]; ok {
fmt.Fprint(w, description)
} else {
webutils.ProblemDetailsStatusOnly(w, 404)
}
}

View file

@ -30,6 +30,10 @@ import (
"context"
"fmt"
"net/http"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/linstrom/web/public/api"
)
type Server struct {
@ -42,8 +46,13 @@ func New(addr string) *Server {
w.WriteHeader(500)
fmt.Fprint(w, "not implemented")
})
handler.Handle("/api/", http.StripPrefix("/api", api.BuildApiRouter()))
handler.HandleFunc("GET /.well-known/webfinger", api.WellKnownWebfinger)
handler.HandleFunc("GET /.well-known/nodeinfo", api.WellKnownNodeinfo)
handler.HandleFunc("GET /nodeinfo/2.1", api.Nodeinfo)
handler.HandleFunc("GET /errors/{name}", errorTypeHandler)
server := http.Server{
Handler: handler,
Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware),
Addr: addr,
}
return &Server{&server}