Add debug handler for fetching a remote actor
All checks were successful
/ docker (push) Successful in 3m59s
All checks were successful
/ docker (push) Successful in 3m59s
Will be used later to add to internal db
This commit is contained in:
parent
d4f2f66807
commit
f8b3a6ff06
12 changed files with 313 additions and 156 deletions
101
activitypub/import.go
Normal file
101
activitypub/import.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package activitypub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
apshared "git.mstar.dev/mstar/linstrom/activitypub/shared"
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ImportRemoteAccount(targetName string) (string, error) {
|
||||||
|
type InboundUserKey struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Pem string `json:"publicKeyPem"`
|
||||||
|
}
|
||||||
|
type InboundUserMedia struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
}
|
||||||
|
type InboundUser struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
PreferredUsername string `json:"preferredUsername"`
|
||||||
|
Inbox string `json:"inbox"`
|
||||||
|
PublicKey *InboundUserKey `json:"publicKey"`
|
||||||
|
Published *time.Time `json:"published"`
|
||||||
|
DisplayName *string `json:"name"`
|
||||||
|
Description *string `json:"summary,omitempty"`
|
||||||
|
PublicUrl *string `json:"url"`
|
||||||
|
Icon *InboundUserMedia `json:"icon,omitempty"`
|
||||||
|
Banner *InboundUserMedia `json:"image,omitempty"`
|
||||||
|
Discoverable *bool `json:"discoverable"`
|
||||||
|
Location *string `json:"vcard:Address,omitempty"`
|
||||||
|
Birthday *string `json:"vcard:bday,omitempty"`
|
||||||
|
SpeakAsCat bool `json:"speakAsCat"`
|
||||||
|
IsCat bool `json:"isCat"`
|
||||||
|
RestrictedFollow *bool `json:"manuallyApprovesFollowers"`
|
||||||
|
}
|
||||||
|
// Get the target user's link first
|
||||||
|
webfinger, err := apshared.GetAccountWebfinger(targetName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
selfLinks := sliceutils.Filter(webfinger.Links, func(t apshared.LinkData) bool {
|
||||||
|
return t.Relation == "self"
|
||||||
|
})
|
||||||
|
if len(selfLinks) == 0 {
|
||||||
|
return "", errors.New("No self link")
|
||||||
|
}
|
||||||
|
APLink := selfLinks[0]
|
||||||
|
req, err := http.NewRequest("GET", *APLink.Href, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/activity+json")
|
||||||
|
|
||||||
|
// Server actor key for signing
|
||||||
|
linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq("linstrom")).First()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var keyBytes []byte
|
||||||
|
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
||||||
|
keyBytes = linstromActor.PrivateKeyEd
|
||||||
|
} else {
|
||||||
|
keyBytes = linstromActor.PrivateKeyRsa
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign and send
|
||||||
|
err = webshared.SignRequest(req, linstromActor.ID+"#main-key", keyBytes, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
response, err := webshared.RequestClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return "", errors.New("Bad status")
|
||||||
|
}
|
||||||
|
var data InboundUser
|
||||||
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
log.Info().Bytes("body", body).Msg("Body from request")
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
log.Info().Any("received-data", data).Msg("Response data")
|
||||||
|
return "", nil
|
||||||
|
}
|
47
activitypub/shared/types/context.go
Normal file
47
activitypub/shared/types/context.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
var BaseLdContext = []any{
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
map[string]any{
|
||||||
|
"Key": "sec:Key",
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"quoteUrl": "as:quoteUrl",
|
||||||
|
"fedibird": "http://fedibird.com/ns#",
|
||||||
|
"quoteUri": "fedibird:quoteUri",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"Emoji": "toot:Emoji",
|
||||||
|
"featured": "toot:featured",
|
||||||
|
"discoverable": "toot:discoverable",
|
||||||
|
"indexable": "toot:indexable",
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value",
|
||||||
|
"misskey": "https://misskey-hub.net/ns#",
|
||||||
|
"_misskey_content": "misskey:_misskey_content",
|
||||||
|
"_misskey_quote": "misskey:_misskey_quote",
|
||||||
|
"_misskey_reaction": "misskey:_misskey_reaction",
|
||||||
|
"_misskey_votes": "misskey:_misskey_votes",
|
||||||
|
"_misskey_summary": "misskey:_misskey_summary",
|
||||||
|
"_misskey_followedMessage": "misskey:_misskey_followedMessage",
|
||||||
|
"_misskey_requireSigninToViewContents": "misskey:_misskey_requireSigninToViewContents",
|
||||||
|
"_misskey_makeNotesFollowersOnlyBefore": "misskey:_misskey_makeNotesFollowersOnlyBefore",
|
||||||
|
"_misskey_makeNotesHiddenBefore": "misskey:_misskey_makeNotesHiddenBefore",
|
||||||
|
"_misskey_license": "misskey:_misskey_license",
|
||||||
|
"freeText": map[string]string{
|
||||||
|
"@id": "misskey:freeText",
|
||||||
|
"@type": "schema:text",
|
||||||
|
},
|
||||||
|
"isCat": "misskey:isCat",
|
||||||
|
"firefish": "https://joinfirefish.org/ns#",
|
||||||
|
"speakAsCat": "firefish:speakAsCat",
|
||||||
|
"sharkey": "https://joinsharkey.org/ns#",
|
||||||
|
"hideOnlineStatus": "sharkey:hideOnlineStatus",
|
||||||
|
"backgroundUrl": "sharkey:backgroundUrl",
|
||||||
|
"listenbrainz": "sharkey:listenbrainz",
|
||||||
|
"enableRss": "sharkey:enableRss",
|
||||||
|
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package ap
|
package apshared
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ func (i InvalidFullHandleError) Error() string {
|
||||||
func SplitFullHandle(full string) (string, string, error) {
|
func SplitFullHandle(full string) (string, string, error) {
|
||||||
splits := strings.Split(strings.TrimPrefix(full, "@"), "@")
|
splits := strings.Split(strings.TrimPrefix(full, "@"), "@")
|
||||||
if len(splits) != 2 {
|
if len(splits) != 2 {
|
||||||
return "", "", InvalidFullHandleError{}
|
return "", "", InvalidFullHandleError{full}
|
||||||
}
|
}
|
||||||
return splits[0], splits[1], nil
|
return splits[0], splits[1], nil
|
||||||
}
|
}
|
|
@ -1,32 +1,37 @@
|
||||||
package ap
|
package apshared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNoApUrl = errors.New("no Activitypub url in webfinger")
|
||||||
|
|
||||||
|
type LinkData struct {
|
||||||
|
// What type of link this is
|
||||||
|
// - `self` refers to the activitypub object and has Type and Href set
|
||||||
|
// - `http://webfinger.net/rel/profile-page` refers to the public webpage of the account and has Type and Href set
|
||||||
|
// - `http://ostatus.org/schema/1.0/subscribe` provides a template for subscribing/following the account. Has Template set
|
||||||
|
// Template will contain a `{uri}` part with which to replace idk yet
|
||||||
|
Relation string `json:"rel"`
|
||||||
|
// The content type of the url
|
||||||
|
Type *string `json:"type"`
|
||||||
|
// The url
|
||||||
|
Href *string `json:"href"`
|
||||||
|
// Template to use for something
|
||||||
|
Template *string `json:"template"`
|
||||||
|
}
|
||||||
|
|
||||||
// Data returned from a webfinger response (and also sent when asked for an account via webfinger)
|
// Data returned from a webfinger response (and also sent when asked for an account via webfinger)
|
||||||
type WebfingerData struct {
|
type WebfingerData struct {
|
||||||
// What this webfinger data refers to. Accounts are usually `acct:username@host`
|
// What this webfinger data refers to. Accounts are usually `acct:username@host`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
// List of links related to the account
|
// List of links related to the account
|
||||||
Links []struct {
|
Links []LinkData `json:"links"`
|
||||||
// What type of link this is
|
|
||||||
// - `self` refers to the activitypub object and has Type and Href set
|
|
||||||
// - `http://webfinger.net/rel/profile-page` refers to the public webpage of the account and has Type and Href set
|
|
||||||
// - `http://ostatus.org/schema/1.0/subscribe` provides a template for subscribing/following the account. Has Template set
|
|
||||||
// Template will contain a `{uri}` part with which to replace idk yet
|
|
||||||
Relation string `json:"rel"`
|
|
||||||
// The content type of the url
|
|
||||||
Type *string `json:"type"`
|
|
||||||
// The url
|
|
||||||
Href *string `json:"href"`
|
|
||||||
// Template to use for something
|
|
||||||
Template *string `json:"template"`
|
|
||||||
} `json:"links"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrAccountNotFound = errors.New("account not found")
|
var ErrAccountNotFound = errors.New("account not found")
|
||||||
|
@ -53,10 +58,8 @@ func GetAccountWebfinger(fullHandle string) (*WebfingerData, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Make a http client with a timeout limit of 30 seconds
|
|
||||||
client := http.Client{Timeout: time.Second * 30}
|
|
||||||
// Then send the request
|
// Then send the request
|
||||||
result, err := client.Do(webfingerRequest)
|
result, err := webshared.RequestClient.Do(webfingerRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
|
@ -1,85 +0,0 @@
|
||||||
package ap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.mstar.dev/mstar/goap"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNoApUrl = errors.New("no Activitypub url in webfinger")
|
|
||||||
|
|
||||||
func GetRemoteUser(fullHandle string) (goap.BaseApChain, error) {
|
|
||||||
webfinger, err := GetAccountWebfinger(fullHandle)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
apUrl := ""
|
|
||||||
for _, link := range webfinger.Links {
|
|
||||||
if link.Relation == "self" {
|
|
||||||
apUrl = *link.Href
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if apUrl == "" {
|
|
||||||
return nil, ErrNoApUrl
|
|
||||||
}
|
|
||||||
apRequest, err := http.NewRequest("GET", apUrl, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
apRequest.Header.Add("Accept", "application/activity+json,application/ld+json,application/json")
|
|
||||||
client := http.Client{Timeout: time.Second * 30}
|
|
||||||
res, err := client.Do(apRequest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
apObject, _ := goap.Unmarshal(body, nil, nil)
|
|
||||||
// Check if Id exists
|
|
||||||
if _, ok := goap.FindAttribute[*goap.UDIdData](apObject); !ok {
|
|
||||||
return nil, fmt.Errorf("missing attribute for account: Id")
|
|
||||||
}
|
|
||||||
// Check that it has the correct object type for an account
|
|
||||||
if objTypePtr, ok := goap.FindAttribute[*goap.UDTypeData](apObject); !ok {
|
|
||||||
return nil, fmt.Errorf("missing attribute for account: Type")
|
|
||||||
} else if objType := *objTypePtr; objType.Type != goap.KEY_ACTIVITYSTREAMS_ACTOR {
|
|
||||||
return nil, fmt.Errorf("wrong ap object type: %s", objType.Type)
|
|
||||||
}
|
|
||||||
// And finally check for inbox
|
|
||||||
if _, ok := goap.FindAttribute[*goap.W3InboxData](apObject); !ok {
|
|
||||||
return nil, fmt.Errorf("missing attribute for account: Inbox")
|
|
||||||
}
|
|
||||||
return apObject, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRemoteObject(target string) (goap.BaseApChain, error) {
|
|
||||||
apRequest, err := http.NewRequest("GET", target, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
apRequest.Header.Add("Accept", "application/activity+json,application/ld+json,application/json")
|
|
||||||
client := http.Client{Timeout: time.Second * 30}
|
|
||||||
res, err := client.Do(apRequest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
apObject, _ := goap.Unmarshal(body, nil, nil)
|
|
||||||
return apObject, nil
|
|
||||||
}
|
|
|
@ -202,6 +202,9 @@ var defaultConfig Config = Config{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the full domain name of the server, as specified via the config.
|
||||||
|
//
|
||||||
|
// Example: "git.mstar.dev" (with subdomain = "git" and domain = "mstar.dev")
|
||||||
func (gc *ConfigGeneral) GetFullDomain() string {
|
func (gc *ConfigGeneral) GetFullDomain() string {
|
||||||
if gc.Subdomain != nil {
|
if gc.Subdomain != nil {
|
||||||
return *gc.Subdomain + gc.Domain
|
return *gc.Subdomain + gc.Domain
|
||||||
|
@ -209,6 +212,11 @@ func (gc *ConfigGeneral) GetFullDomain() string {
|
||||||
return gc.Domain
|
return gc.Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the public root url of the server, including port (if needed, public takes precedence if set)
|
||||||
|
// and set protocol
|
||||||
|
//
|
||||||
|
// Example: "http://git.mstar.dev:5722" (with Subdomain = "git", domain = "mstar.dev",
|
||||||
|
// privatePort = 34546, publicPort = 5722 and protocol = "http")
|
||||||
func (gc *ConfigGeneral) GetFullPublicUrl() string {
|
func (gc *ConfigGeneral) GetFullPublicUrl() string {
|
||||||
str := gc.Protocol + "://" + gc.GetFullDomain()
|
str := gc.Protocol + "://" + gc.GetFullDomain()
|
||||||
if gc.PublicPort != nil {
|
if gc.PublicPort != nil {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/ap"
|
ap "git.mstar.dev/mstar/linstrom/activitypub/shared"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
|
@ -27,6 +27,7 @@ func New(addr string) *Server {
|
||||||
handler.HandleFunc("GET /delete", deleteUser)
|
handler.HandleFunc("GET /delete", deleteUser)
|
||||||
handler.HandleFunc("POST /post-as", postAs)
|
handler.HandleFunc("POST /post-as", postAs)
|
||||||
handler.HandleFunc("GET /notes-for", notesFrom)
|
handler.HandleFunc("GET /notes-for", notesFrom)
|
||||||
|
handler.HandleFunc("GET /import", issueUserImport)
|
||||||
web := http.Server{
|
web := http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: webutils.ChainMiddlewares(
|
Handler: webutils.ChainMiddlewares(
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
@ -171,3 +172,19 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
dbgen.User.Where(dbgen.User.ID.Eq(id)).Delete()
|
dbgen.User.Where(dbgen.User.ID.Eq(id)).Delete()
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func issueUserImport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
target := r.FormValue("target")
|
||||||
|
_, err := activitypub.ImportRemoteAccount(target)
|
||||||
|
hlog.FromRequest(r).Info().Err(err).Msg("Err from import request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func kickoffFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type Inbound struct {
|
||||||
|
Id string
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
var data Inbound
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.Decode(&data)
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub/shared/types"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
@ -19,52 +20,6 @@ import (
|
||||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
var baseLdContext = []any{
|
|
||||||
"https://www.w3.org/ns/activitystreams",
|
|
||||||
"https://w3id.org/security/v1",
|
|
||||||
map[string]any{
|
|
||||||
"Key": "sec:Key",
|
|
||||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
|
||||||
"sensitive": "as:sensitive",
|
|
||||||
"Hashtag": "as:Hashtag",
|
|
||||||
"quoteUrl": "as:quoteUrl",
|
|
||||||
"fedibird": "http://fedibird.com/ns#",
|
|
||||||
"quoteUri": "fedibird:quoteUri",
|
|
||||||
"toot": "http://joinmastodon.org/ns#",
|
|
||||||
"Emoji": "toot:Emoji",
|
|
||||||
"featured": "toot:featured",
|
|
||||||
"discoverable": "toot:discoverable",
|
|
||||||
"indexable": "toot:indexable",
|
|
||||||
"schema": "http://schema.org#",
|
|
||||||
"PropertyValue": "schema:PropertyValue",
|
|
||||||
"value": "schema:value",
|
|
||||||
"misskey": "https://misskey-hub.net/ns#",
|
|
||||||
"_misskey_content": "misskey:_misskey_content",
|
|
||||||
"_misskey_quote": "misskey:_misskey_quote",
|
|
||||||
"_misskey_reaction": "misskey:_misskey_reaction",
|
|
||||||
"_misskey_votes": "misskey:_misskey_votes",
|
|
||||||
"_misskey_summary": "misskey:_misskey_summary",
|
|
||||||
"_misskey_followedMessage": "misskey:_misskey_followedMessage",
|
|
||||||
"_misskey_requireSigninToViewContents": "misskey:_misskey_requireSigninToViewContents",
|
|
||||||
"_misskey_makeNotesFollowersOnlyBefore": "misskey:_misskey_makeNotesFollowersOnlyBefore",
|
|
||||||
"_misskey_makeNotesHiddenBefore": "misskey:_misskey_makeNotesHiddenBefore",
|
|
||||||
"_misskey_license": "misskey:_misskey_license",
|
|
||||||
"freeText": map[string]string{
|
|
||||||
"@id": "misskey:freeText",
|
|
||||||
"@type": "schema:text",
|
|
||||||
},
|
|
||||||
"isCat": "misskey:isCat",
|
|
||||||
"firefish": "https://joinfirefish.org/ns#",
|
|
||||||
"speakAsCat": "firefish:speakAsCat",
|
|
||||||
"sharkey": "https://joinsharkey.org/ns#",
|
|
||||||
"hideOnlineStatus": "sharkey:hideOnlineStatus",
|
|
||||||
"backgroundUrl": "sharkey:backgroundUrl",
|
|
||||||
"listenbrainz": "sharkey:listenbrainz",
|
|
||||||
"enableRss": "sharkey:enableRss",
|
|
||||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func users(w http.ResponseWriter, r *http.Request) {
|
func users(w http.ResponseWriter, r *http.Request) {
|
||||||
type OutboundKey struct {
|
type OutboundKey struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
@ -120,7 +75,7 @@ func users(w http.ResponseWriter, r *http.Request) {
|
||||||
keyBytes = keyBytesToPem(user.PublicKeyRsa)
|
keyBytes = keyBytesToPem(user.PublicKeyRsa)
|
||||||
}
|
}
|
||||||
data := Outbound{
|
data := Outbound{
|
||||||
Context: baseLdContext,
|
Context: types.BaseLdContext,
|
||||||
Id: apUrl,
|
Id: apUrl,
|
||||||
Type: "Person",
|
Type: "Person",
|
||||||
PreferredUsername: user.Username,
|
PreferredUsername: user.Username,
|
||||||
|
|
79
web/shared/client.go
Normal file
79
web/shared/client.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package webshared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/goutils/maputils"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// No init needed, zero value is good
|
||||||
|
|
||||||
|
var RequestClient http.Client
|
||||||
|
|
||||||
|
const xRandomHeader = "X-Auth-Random"
|
||||||
|
|
||||||
|
// Sign a given outbound request for authorized fetch.
|
||||||
|
// At the end, the Signature header will have the signature needed,
|
||||||
|
// nothing else is modified.
|
||||||
|
// If the request is POST, the postBody must contain the raw body of
|
||||||
|
// the request and the Digest header will also be added
|
||||||
|
func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte) error {
|
||||||
|
method := r.Method
|
||||||
|
headers := r.Header
|
||||||
|
var nowString string
|
||||||
|
if dateString := headers.Get("Date"); dateString != "" {
|
||||||
|
nowString = dateString
|
||||||
|
} else {
|
||||||
|
nowString = time.Now().Format("Mon, 02 Jan 2006 15:04:05 MST")
|
||||||
|
headers.Set("Date", nowString)
|
||||||
|
}
|
||||||
|
var host string
|
||||||
|
if hostString := headers.Get("Host"); hostString != "" {
|
||||||
|
host = hostString
|
||||||
|
} else {
|
||||||
|
host = config.GlobalConfig.General.GetFullDomain()
|
||||||
|
headers.Set("Date", host)
|
||||||
|
}
|
||||||
|
applyBodyHash(headers, postBody)
|
||||||
|
mappedHeaders := maputils.MapSameKeys(headers, func(k string, v []string) string {
|
||||||
|
if len(v) > 0 {
|
||||||
|
return v[0]
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var signedString string
|
||||||
|
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
||||||
|
tmp, err := CreateSignatureED(method, r.URL.RawPath, mappedHeaders, privateKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signedString = tmp
|
||||||
|
} else {
|
||||||
|
tmp, err := CreateSignatureRSA(method, r.URL.RawPath, mappedHeaders, privateKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signedString = tmp
|
||||||
|
}
|
||||||
|
signature := CreateSignatureHeaderContent(
|
||||||
|
keyId,
|
||||||
|
signedString,
|
||||||
|
maputils.KeysFromMap(mappedHeaders)...,
|
||||||
|
)
|
||||||
|
headers.Set("Signature", signature)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyBodyHash(headers http.Header, body []byte) error {
|
||||||
|
if body == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hash := sha256.Sum256(body)
|
||||||
|
headers.Set("Digest", string(hash[:]))
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
package webshared
|
package webshared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Generate the signed string of the headers, method and target
|
||||||
|
// and sign it using the given RSA key. Returns the base64 encoded
|
||||||
|
// result
|
||||||
func CreateSignatureRSA(
|
func CreateSignatureRSA(
|
||||||
method string,
|
method string,
|
||||||
target string,
|
target string,
|
||||||
|
@ -14,9 +18,12 @@ func CreateSignatureRSA(
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
message := genPreSignatureString(method, target, headers)
|
message := genPreSignatureString(method, target, headers)
|
||||||
signed, err := shared.Sign(message, privateKeyBytes, true)
|
signed, err := shared.Sign(message, privateKeyBytes, true)
|
||||||
return string(signed), err
|
return base64.StdEncoding.EncodeToString(signed), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate the signed string of the headers, method and target
|
||||||
|
// and sign it using the given ED25519 key. Returns the base64
|
||||||
|
// encoded result
|
||||||
func CreateSignatureED(
|
func CreateSignatureED(
|
||||||
method string,
|
method string,
|
||||||
target string,
|
target string,
|
||||||
|
@ -25,7 +32,10 @@ func CreateSignatureED(
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
message := genPreSignatureString(method, target, headers)
|
message := genPreSignatureString(method, target, headers)
|
||||||
signed, err := shared.Sign(message, privateKeyBytes, false)
|
signed, err := shared.Sign(message, privateKeyBytes, false)
|
||||||
return string(signed), err
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(signed), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func genPreSignatureString(method, target string, headers map[string]string) string {
|
func genPreSignatureString(method, target string, headers map[string]string) string {
|
||||||
|
@ -38,3 +48,24 @@ func genPreSignatureString(method, target string, headers map[string]string) str
|
||||||
}
|
}
|
||||||
return dataBuilder.String()
|
return dataBuilder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate the content of the "Signature" header based on
|
||||||
|
// The user who's key was used, the hashed and base64 encoded
|
||||||
|
// signed string, as returned by CreateSignatureED/RSA
|
||||||
|
func CreateSignatureHeaderContent(userId string, hash string, headerNames ...string) string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
builder.WriteString("keyId=\"")
|
||||||
|
builder.WriteString(userId)
|
||||||
|
builder.WriteString("\",headers=\"")
|
||||||
|
for i, header := range headerNames {
|
||||||
|
builder.WriteString(header)
|
||||||
|
if i+1 < len(headerNames) {
|
||||||
|
builder.WriteRune(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.WriteString("\",signature=\"")
|
||||||
|
builder.WriteString(hash)
|
||||||
|
builder.WriteRune('"')
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue