More work on getting auth fetch verification working
This commit is contained in:
parent
7eac1db475
commit
9957ba8302
12 changed files with 434 additions and 205 deletions
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -52,7 +53,7 @@ type inboundImportUser struct {
|
|||
RestrictedFollow *bool `json:"manuallyApprovesFollowers"`
|
||||
}
|
||||
|
||||
func ImportRemoteAccount(targetName string) (string, error) {
|
||||
func ImportRemoteAccountByHandle(targetName string) (string, error) {
|
||||
// Get the target user's link first
|
||||
webfinger, err := GetAccountWebfinger(targetName)
|
||||
if err != nil {
|
||||
|
@ -65,177 +66,11 @@ func ImportRemoteAccount(targetName string) (string, error) {
|
|||
return "", errors.New("no self link")
|
||||
}
|
||||
APLink := selfLinks[0]
|
||||
// Server actor key for signing
|
||||
linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq(shared.ServerActorName)).First()
|
||||
acc, err := ImportRemoteAccountByAPUrl(*APLink.Href)
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to get server actor", err)
|
||||
return "", err
|
||||
}
|
||||
var response *http.Response
|
||||
response, err = webshared.RequestSignedCavage("GET", *APLink.Href, nil, linstromActor)
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to complete cavage signed request", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
log.Debug().
|
||||
Int("status", response.StatusCode).
|
||||
Bytes("body", body).
|
||||
Any("headers", response.Header).
|
||||
Msg("Response information")
|
||||
if response.StatusCode != 200 {
|
||||
return "", fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode)
|
||||
}
|
||||
var data inboundImportUser
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to unmarshal response", err)
|
||||
}
|
||||
log.Debug().Any("received-data", data).Msg("Response data")
|
||||
|
||||
// TODO: Store received user in db
|
||||
_, host, _ := SplitFullHandle(targetName)
|
||||
hostId, err := ImportRemoteServer(host)
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to import host of target user", err)
|
||||
}
|
||||
|
||||
user, err := dbgen.User.
|
||||
Where(dbgen.User.Username.Eq(targetName)).
|
||||
Where(dbgen.User.ServerId.Eq(hostId)).
|
||||
Preload(dbgen.User.RemoteInfo).
|
||||
Preload(dbgen.User.InfoFields).
|
||||
Preload(dbgen.User.BeingTypes).
|
||||
Preload(dbgen.User.Roles).
|
||||
FirstOrCreate()
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to find or create user in db", err)
|
||||
}
|
||||
user.Verified = true
|
||||
user.FinishedRegistration = true
|
||||
|
||||
if !sliceutils.ContainsFunc(user.Roles, func(t models.UserToRole) bool {
|
||||
return t.Role.ID == models.DefaultUserRole.ID
|
||||
}) {
|
||||
roleMapping := models.UserToRole{
|
||||
Role: models.DefaultUserRole,
|
||||
RoleId: models.DefaultUserRole.ID,
|
||||
UserId: user.ID,
|
||||
}
|
||||
if err = dbgen.UserToRole.Create(&roleMapping); err != nil {
|
||||
return "", other.Error(
|
||||
"activitypub",
|
||||
"failed to store default user role to imported account mapping",
|
||||
err,
|
||||
)
|
||||
}
|
||||
if err = dbgen.User.Roles.Model(user).Append(&roleMapping); err != nil {
|
||||
return "", other.Error("activitypub", "failed to attach default role to user", err)
|
||||
}
|
||||
}
|
||||
if user.RemoteInfo == nil {
|
||||
user.RemoteInfo = &models.UserRemoteLinks{
|
||||
UserId: user.ID,
|
||||
ApLink: data.Id,
|
||||
}
|
||||
err = dbgen.UserRemoteLinks.Create(user.RemoteInfo)
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to create remote data for user", err)
|
||||
}
|
||||
err = dbgen.User.RemoteInfo.Model(user).Replace(user.RemoteInfo)
|
||||
if err != nil {
|
||||
return "", other.Error("activitypub", "failed to connect remote data to user", err)
|
||||
}
|
||||
user.RemoteInfoId.Int64 = int64(user.RemoteInfo.ID)
|
||||
user.RemoteInfoId.Valid = true
|
||||
}
|
||||
if data.DisplayName != nil {
|
||||
user.DisplayName = *data.DisplayName
|
||||
}
|
||||
if data.Outbox != nil {
|
||||
user.RemoteInfo.OutboxLink.String = *data.Outbox
|
||||
user.RemoteInfo.OutboxLink.Valid = true
|
||||
}
|
||||
user.RemoteInfo.InboxLink = data.Inbox
|
||||
if data.PublicKey != nil {
|
||||
pemBlock, _ := pem.Decode([]byte(data.PublicKey.Pem))
|
||||
if pemBlock.Type != "PUBLIC KEY" && pemBlock.Type != "RSA PUBLIC KEY" {
|
||||
return "", fmt.Errorf("activitypub: invalid public key block type: %v", pemBlock.Type)
|
||||
}
|
||||
user.PublicKeyRsa = pemBlock.Bytes
|
||||
}
|
||||
// Assume published day of user won't change
|
||||
if data.Description != nil {
|
||||
user.Description = *data.Description
|
||||
}
|
||||
if data.PublicUrl != nil {
|
||||
user.RemoteInfo.ViewLink.String = *data.PublicUrl
|
||||
user.RemoteInfo.ViewLink.Valid = true
|
||||
}
|
||||
if data.Discoverable != nil {
|
||||
user.Indexable = *data.Discoverable
|
||||
} else {
|
||||
// Assume false per default
|
||||
user.Indexable = false
|
||||
}
|
||||
if data.Location != nil {
|
||||
user.Location.String = *data.Location
|
||||
user.Location.Valid = true
|
||||
}
|
||||
if data.Birthday != nil {
|
||||
user.Birthday.String = *data.Birthday
|
||||
user.Birthday.Valid = true
|
||||
}
|
||||
if data.RestrictedFollow != nil {
|
||||
user.RestrictedFollow = *data.RestrictedFollow
|
||||
} else {
|
||||
// Assume not restricted if not included in received data
|
||||
user.RestrictedFollow = false
|
||||
}
|
||||
if data.IsCat {
|
||||
if !sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
||||
return t.Being == string(models.BEING_CAT)
|
||||
}) {
|
||||
log.Debug().Msg("user doesn't contain cat yet")
|
||||
bt := models.UserToBeing{UserId: user.ID, Being: string(models.BEING_CAT)}
|
||||
if err = dbgen.UserToBeing.Create(&bt); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to append cat being type to imported user")
|
||||
}
|
||||
if err = dbgen.User.BeingTypes.Model(user).Append(&bt); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("Failed to append cat being type to imported user relations")
|
||||
}
|
||||
user.BeingTypes = append(user.BeingTypes, bt)
|
||||
}
|
||||
} else {
|
||||
if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
||||
return t.Being == string(models.BEING_CAT)
|
||||
}) {
|
||||
_, err = dbgen.UserToBeing.Where(dbgen.UserToBeing.UserId.Eq(user.ID)).Where(dbgen.UserToBeing.Being.Eq(string(models.BEING_CAT))).Delete()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to remove cat being type from user")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't handle SpeakAsCat yet, as not included in stored data yet
|
||||
|
||||
// Handle media last as more complicated
|
||||
// Icon *inboundImportUserMedia `json:"icon,omitempty"`
|
||||
// Banner *inboundImportUserMedia `json:"image,omitempty"`
|
||||
|
||||
id := user.ID
|
||||
user.ID = ""
|
||||
if _, err = dbgen.User.Where(dbgen.User.ID.Eq(id)).UpdateColumns(user); err != nil {
|
||||
return "", other.Error("activitypub", "failed to update imported user", err)
|
||||
}
|
||||
rlid := user.RemoteInfo.ID
|
||||
user.RemoteInfo.ID = 0
|
||||
if _, err = dbgen.UserRemoteLinks.Where(dbgen.UserRemoteLinks.ID.Eq(rlid)).UpdateColumns(user.RemoteInfo); err != nil {
|
||||
return "", other.Error("activitypub", "failed to update imported user's remote links", err)
|
||||
}
|
||||
|
||||
log.Info().Str("user", targetName).Msg("Import completed")
|
||||
return id, nil
|
||||
return acc.ID, nil
|
||||
}
|
||||
|
||||
func ImportRemoteServer(host string) (uint, error) {
|
||||
|
@ -282,7 +117,7 @@ func ImportRemoteServer(host string) (uint, error) {
|
|||
if err != nil {
|
||||
return 0, other.Error("activitypub", "failed to unmarshal info", err)
|
||||
}
|
||||
log.Debug().Any("nodeinfo", data).Msg("Server info received")
|
||||
// log.Debug().Any("nodeinfo", data).Msg("Server info received")
|
||||
rs := dbgen.RemoteServer
|
||||
// rsm := dbgen.RemoteServerMetadata
|
||||
serverModelType := webshared.MapNodeServerTypeToModelType(data.Software.Name)
|
||||
|
@ -571,3 +406,181 @@ func ImportRemoteServer(host string) (uint, error) {
|
|||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) {
|
||||
log.Info().Str("ap-url", apUrl).Msg("Importing account by ap url")
|
||||
// Server actor key for signing
|
||||
linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq(shared.ServerActorName)).First()
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to get server actor", err)
|
||||
}
|
||||
var response *http.Response
|
||||
response, err = webshared.RequestSignedCavage("GET", apUrl, nil, linstromActor)
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to complete cavage signed request", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
log.Debug().
|
||||
Int("status", response.StatusCode).
|
||||
Bytes("body", body).
|
||||
// Any("headers", response.Header).
|
||||
Msg("Response information")
|
||||
if response.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode)
|
||||
}
|
||||
var data inboundImportUser
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to unmarshal response", err)
|
||||
}
|
||||
// log.Debug().Any("received-data", data).Msg("Response data")
|
||||
|
||||
targetUrl, err := url.Parse(apUrl)
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to parse url as url", err)
|
||||
}
|
||||
hostId, err := ImportRemoteServer(targetUrl.Host)
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to import host of target user", err)
|
||||
}
|
||||
|
||||
user, err := dbgen.User.
|
||||
Where(dbgen.User.Username.Eq(data.PreferredUsername + "@" + targetUrl.Host)).
|
||||
Where(dbgen.User.ServerId.Eq(hostId)).
|
||||
Preload(dbgen.User.RemoteInfo).
|
||||
Preload(dbgen.User.InfoFields).
|
||||
Preload(dbgen.User.BeingTypes).
|
||||
Preload(dbgen.User.Roles).
|
||||
FirstOrCreate()
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to find or create user in db", err)
|
||||
}
|
||||
user.Verified = true
|
||||
user.FinishedRegistration = true
|
||||
|
||||
if !sliceutils.ContainsFunc(user.Roles, func(t models.UserToRole) bool {
|
||||
return t.Role.ID == models.DefaultUserRole.ID
|
||||
}) {
|
||||
roleMapping := models.UserToRole{
|
||||
Role: models.DefaultUserRole,
|
||||
RoleId: models.DefaultUserRole.ID,
|
||||
UserId: user.ID,
|
||||
}
|
||||
if err = dbgen.UserToRole.Create(&roleMapping); err != nil {
|
||||
return nil, other.Error(
|
||||
"activitypub",
|
||||
"failed to store default user role to imported account mapping",
|
||||
err,
|
||||
)
|
||||
}
|
||||
if err = dbgen.User.Roles.Model(user).Append(&roleMapping); err != nil {
|
||||
return nil, other.Error("activitypub", "failed to attach default role to user", err)
|
||||
}
|
||||
}
|
||||
if user.RemoteInfo == nil {
|
||||
user.RemoteInfo = &models.UserRemoteLinks{
|
||||
UserId: user.ID,
|
||||
ApLink: data.Id,
|
||||
}
|
||||
err = dbgen.UserRemoteLinks.Create(user.RemoteInfo)
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to create remote data for user", err)
|
||||
}
|
||||
err = dbgen.User.RemoteInfo.Model(user).Replace(user.RemoteInfo)
|
||||
if err != nil {
|
||||
return nil, other.Error("activitypub", "failed to connect remote data to user", err)
|
||||
}
|
||||
user.RemoteInfoId.Int64 = int64(user.RemoteInfo.ID)
|
||||
user.RemoteInfoId.Valid = true
|
||||
}
|
||||
if data.DisplayName != nil {
|
||||
user.DisplayName = *data.DisplayName
|
||||
}
|
||||
if data.Outbox != nil {
|
||||
user.RemoteInfo.OutboxLink.String = *data.Outbox
|
||||
user.RemoteInfo.OutboxLink.Valid = true
|
||||
}
|
||||
user.RemoteInfo.InboxLink = data.Inbox
|
||||
if data.PublicKey != nil {
|
||||
pemBlock, _ := pem.Decode([]byte(data.PublicKey.Pem))
|
||||
if pemBlock.Type != "PUBLIC KEY" && pemBlock.Type != "RSA PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("activitypub: invalid public key block type: %v", pemBlock.Type)
|
||||
}
|
||||
user.PublicKeyRsa = pemBlock.Bytes
|
||||
}
|
||||
// Assume published day of user won't change
|
||||
if data.Description != nil {
|
||||
user.Description = *data.Description
|
||||
}
|
||||
if data.PublicUrl != nil {
|
||||
user.RemoteInfo.ViewLink.String = *data.PublicUrl
|
||||
user.RemoteInfo.ViewLink.Valid = true
|
||||
}
|
||||
if data.Discoverable != nil {
|
||||
user.Indexable = *data.Discoverable
|
||||
} else {
|
||||
// Assume false per default
|
||||
user.Indexable = false
|
||||
}
|
||||
if data.Location != nil {
|
||||
user.Location.String = *data.Location
|
||||
user.Location.Valid = true
|
||||
}
|
||||
if data.Birthday != nil {
|
||||
user.Birthday.String = *data.Birthday
|
||||
user.Birthday.Valid = true
|
||||
}
|
||||
if data.RestrictedFollow != nil {
|
||||
user.RestrictedFollow = *data.RestrictedFollow
|
||||
} else {
|
||||
// Assume not restricted if not included in received data
|
||||
user.RestrictedFollow = false
|
||||
}
|
||||
if data.IsCat {
|
||||
if !sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
||||
return t.Being == string(models.BEING_CAT)
|
||||
}) {
|
||||
log.Debug().Msg("user doesn't contain cat yet")
|
||||
bt := models.UserToBeing{UserId: user.ID, Being: string(models.BEING_CAT)}
|
||||
if err = dbgen.UserToBeing.Create(&bt); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to append cat being type to imported user")
|
||||
}
|
||||
if err = dbgen.User.BeingTypes.Model(user).Append(&bt); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("Failed to append cat being type to imported user relations")
|
||||
}
|
||||
user.BeingTypes = append(user.BeingTypes, bt)
|
||||
}
|
||||
} else {
|
||||
if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
||||
return t.Being == string(models.BEING_CAT)
|
||||
}) {
|
||||
_, err = dbgen.UserToBeing.Where(dbgen.UserToBeing.UserId.Eq(user.ID)).Where(dbgen.UserToBeing.Being.Eq(string(models.BEING_CAT))).Delete()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to remove cat being type from user")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't handle SpeakAsCat yet, as not included in stored data yet
|
||||
|
||||
// Handle media last as more complicated
|
||||
// Icon *inboundImportUserMedia `json:"icon,omitempty"`
|
||||
// Banner *inboundImportUserMedia `json:"image,omitempty"`
|
||||
|
||||
id := user.ID
|
||||
user.ID = ""
|
||||
if _, err = dbgen.User.Where(dbgen.User.ID.Eq(id)).UpdateColumns(user); err != nil {
|
||||
return nil, other.Error("activitypub", "failed to update imported user", err)
|
||||
}
|
||||
rlid := user.RemoteInfo.ID
|
||||
user.RemoteInfo.ID = 0
|
||||
if _, err = dbgen.UserRemoteLinks.Where(dbgen.UserRemoteLinks.ID.Eq(rlid)).UpdateColumns(user.RemoteInfo); err != nil {
|
||||
return nil, other.Error("activitypub", "failed to update imported user's remote links", err)
|
||||
}
|
||||
user.ID = id
|
||||
|
||||
log.Info().Str("ap-url", apUrl).Msg("Import completed")
|
||||
return user, nil
|
||||
}
|
||||
|
|
3
main.go
3
main.go
|
@ -135,6 +135,7 @@ func oldServer() {
|
|||
}
|
||||
|
||||
func newServer() {
|
||||
log.Info().Msg("Connectin to db")
|
||||
db, err := gorm.Open(
|
||||
postgres.Open(config.GlobalConfig.Storage.BuildPostgresDSN()),
|
||||
&gorm.Config{
|
||||
|
@ -145,9 +146,11 @@ func newServer() {
|
|||
log.Fatal().Err(err).Msg("Failed to start db")
|
||||
}
|
||||
dbgen.SetDefault(db)
|
||||
log.Info().Msg("Applying migrations")
|
||||
if err = storagenew.Migrate(db); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to automigrate structure")
|
||||
}
|
||||
log.Info().Msg("Inserting self into db")
|
||||
if err = storagenew.InsertSelf(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to insert self properly")
|
||||
}
|
||||
|
|
|
@ -1505,6 +1505,7 @@ type IUserDo interface {
|
|||
GetPagedAllDeleted(pageNr uint) (result []models.User, err error)
|
||||
GetPagedAllNonDeleted(pageNr uint) (result []models.User, err error)
|
||||
GdprUsers() (err error)
|
||||
GetRemoteAccountByApUrl(url string) (result *models.User, err error)
|
||||
}
|
||||
|
||||
// Get a user by a username, ignoring all restrictions on that user
|
||||
|
@ -1636,6 +1637,30 @@ func (u userDo) GdprUsers() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Get user by AP url
|
||||
//
|
||||
// SELECT * FROM @@table
|
||||
//
|
||||
// WHERE id in (
|
||||
// SELECT user_id FROM user_remote_links
|
||||
// WHERE ap_link = @url LIMIT 1
|
||||
// )
|
||||
//
|
||||
// LIMIT 1
|
||||
func (u userDo) GetRemoteAccountByApUrl(url string) (result *models.User, err error) {
|
||||
var params []interface{}
|
||||
|
||||
var generateSQL strings.Builder
|
||||
params = append(params, url)
|
||||
generateSQL.WriteString("SELECT * FROM users WHERE id in ( SELECT user_id FROM user_remote_links WHERE ap_link = ? LIMIT 1 ) LIMIT 1 ")
|
||||
|
||||
var executeSQL *gorm.DB
|
||||
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Take(&result) // ignore_security_alert
|
||||
err = executeSQL.Error
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u userDo) Debug() IUserDo {
|
||||
return u.withDO(u.DO.Debug())
|
||||
}
|
||||
|
|
|
@ -136,4 +136,14 @@ type IUser interface {
|
|||
//
|
||||
// DELETE FROM @@table WHERE deleted_at IS NOT NULL AND deleted_at + interval '30 days' < NOW()
|
||||
GdprUsers() error
|
||||
|
||||
// Get user by AP url
|
||||
//
|
||||
// SELECT * FROM @@table
|
||||
// WHERE id in (
|
||||
// SELECT user_id FROM user_remote_links
|
||||
// WHERE ap_link = @url LIMIT 1
|
||||
// )
|
||||
// LIMIT 1
|
||||
GetRemoteAccountByApUrl(url string) (*gen.T, error)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ import (
|
|||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
)
|
||||
|
||||
// ID of the server actor account in the db.
|
||||
// Set by InsertSelf
|
||||
var ServerActorId = ""
|
||||
|
||||
func InsertSelf() error {
|
||||
if err := insertRoles(); err != nil {
|
||||
return other.Error("storage", "failed to save/update default roles", err)
|
||||
|
@ -30,6 +34,7 @@ func InsertSelf() error {
|
|||
if err != nil {
|
||||
return other.Error("storage", "failed to save/update self user", err)
|
||||
}
|
||||
ServerActorId = user.ID
|
||||
if err = insertUserPronoun(user); err != nil {
|
||||
return other.Error("storage", "failed to save/update self user pronoun", err)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[general]
|
||||
protocol = "https"
|
||||
domain = "lhr.life"
|
||||
subdomain = "3e4af10addc7d0"
|
||||
domain = "serveo.net"
|
||||
subdomain = "38e5543b0fc97680472709952a04622f"
|
||||
private_port = 8080
|
||||
public_port = 443
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ func returnKeypair(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func issueUserImport(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.FormValue("target")
|
||||
_, err := activitypub.ImportRemoteAccount(target)
|
||||
_, err := activitypub.ImportRemoteAccountByHandle(target)
|
||||
hlog.FromRequest(r).Info().Err(err).Msg("Err from import request")
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,23 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
|
||||
webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware"
|
||||
)
|
||||
|
||||
func BuildApiRouter() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.Handle(
|
||||
"/activitypub/",
|
||||
http.StripPrefix("/activitypub", activitypub.BuildActivitypubRouter()),
|
||||
http.StripPrefix(
|
||||
"/activitypub",
|
||||
webutils.ChainMiddlewares(
|
||||
activitypub.BuildActivitypubRouter(),
|
||||
webmiddleware.BuildAuthorizedFetchCheck(true, true),
|
||||
),
|
||||
),
|
||||
)
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "in api")
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
package webmiddleware
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||
)
|
||||
|
||||
const signatureRegexString = `keyId=([a-zA-Z0-9_\-\.:@/\?&=#%\+\[\]!$\(\)\*,;]+),headers="([a-z0-9-_\(\) ]+)",(?:algorithm="([a-z0-9-])+",)?signature="(.+)"`
|
||||
const signatureRegexString = `keyId="([a-zA-Z0-9_\-\.:@\/\?&=#%\+\[\]!$\(\)\*,;]+)",(?:algorithm="([a-z0-9-]+)",)?headers="([a-z0-9-_\(\) ]+)",(?:algorithm="([a-z0-9-]+)",)?signature="(.+)"`
|
||||
|
||||
var publicPaths = []*regexp.Regexp{
|
||||
regexp.MustCompile(`/\.well-known/.+^`),
|
||||
|
@ -27,6 +40,9 @@ var signatureRegex = regexp.MustCompile(signatureRegexString)
|
|||
func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuilder {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := hlog.FromRequest(r)
|
||||
log.Info().Msg("AuthFetch middleware")
|
||||
defer log.Info().Msg("AuhFetch completed")
|
||||
path := r.URL.Path
|
||||
// Check always open path first
|
||||
for _, re := range publicPaths {
|
||||
|
@ -35,17 +51,36 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
|||
return
|
||||
}
|
||||
}
|
||||
// WARN: This check could potentially pose a security risk where an AP request
|
||||
// could get around providing a valid signature by using the server actor's
|
||||
// ID somewhere in the path.
|
||||
// However, I suspect that this risk is mostly mitigated by Go's router
|
||||
// already cleaning paths up and the only other target being one specific
|
||||
// note that most likely won't ever even exist due to the large random value space
|
||||
// offered by UUIDs
|
||||
// So either you get the server actor, which is intended behaviour, or access to
|
||||
// one specific note that most likely won't even exist
|
||||
|
||||
// FIXME: Re-enable once implementation of actual verification is stable
|
||||
// if strings.Contains(path, storage.ServerActorId) {
|
||||
// log.Info().Msg("Server actor requested, no auth")
|
||||
// h.ServeHTTP(w, r)
|
||||
// return
|
||||
// }
|
||||
// Not an always open path, check methods
|
||||
if r.Method == "GET" && !forGet {
|
||||
log.Info().Msg("Get request to AP resources don't need signature")
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
} else if !forGet && !forNonGet {
|
||||
log.Info().Msg("Requests to AP resources don't need signature")
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// TODO: Perform check here
|
||||
log.Info().Msg("Need signature for AP request")
|
||||
signatureHeader := r.Header.Get("Signature")
|
||||
if signatureHeader == "" {
|
||||
log.Info().Msg("Received AP request without signature header where one is required")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
|
@ -56,8 +91,14 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
|||
)
|
||||
return
|
||||
}
|
||||
log.Debug().
|
||||
Str("signature-header", signatureHeader).
|
||||
Msg("Signature header of inbound AP request")
|
||||
match := signatureRegex.FindStringSubmatch(signatureHeader)
|
||||
if len(match) <= 1 {
|
||||
log.Info().
|
||||
Str("header", signatureHeader).
|
||||
Msg("Received signature with invalid pattern")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
|
@ -73,12 +114,22 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
|||
}
|
||||
// fullMatch = match[0]
|
||||
rawKeyId := match[1]
|
||||
rawHeaders := match[2]
|
||||
rawAlgorithm := match[3]
|
||||
signature := match[4]
|
||||
rawAlgorithm1 := match[2]
|
||||
rawHeaders := match[3]
|
||||
rawAlgorithm2 := match[4]
|
||||
signature := match[5]
|
||||
|
||||
var rawAlgorithm string
|
||||
if rawAlgorithm2 != "" {
|
||||
rawAlgorithm = rawAlgorithm2
|
||||
} else if rawAlgorithm1 != "" {
|
||||
rawAlgorithm = rawAlgorithm1
|
||||
} else {
|
||||
rawAlgorithm = "hs2019"
|
||||
}
|
||||
_, err := url.Parse(rawKeyId)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Key id is not an url")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
|
@ -90,15 +141,111 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
|||
return
|
||||
}
|
||||
|
||||
if rawAlgorithm == "" {
|
||||
rawAlgorithm = "hs2019"
|
||||
// w.Header().Add("X-Algorithm-Hint", "")
|
||||
}
|
||||
|
||||
_ = rawHeaders
|
||||
_ = signature
|
||||
_ = rawAlgorithm
|
||||
panic("not implemented")
|
||||
stringToCheck := buildStringToCheck(r, rawHeaders)
|
||||
log.Warn().Str("string-to-check", stringToCheck).Send()
|
||||
|
||||
requestingActor, err := getRequestingActor(rawKeyId)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get requesting actor")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
"/errors/invalid-auth-signature",
|
||||
"invalid authorization signature",
|
||||
other.IntoPointer(
|
||||
"Failed to get the signing account for signature verification",
|
||||
),
|
||||
nil,
|
||||
)
|
||||
return
|
||||
}
|
||||
log.Debug().
|
||||
Str("id", requestingActor.ID).
|
||||
Str("username", requestingActor.Username).
|
||||
Msg("Got requesting actor")
|
||||
|
||||
key, err := x509.ParsePKCS1PublicKey(requestingActor.PublicKeyRsa)
|
||||
if err != nil {
|
||||
rawKey, err := x509.ParsePKIXPublicKey(requestingActor.PublicKeyRsa)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to parse public key of requesting actor")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
"/errors/invalid-auth-signature",
|
||||
"invalid authorization signature",
|
||||
other.IntoPointer("Key is not a valid PKCS1 marshalled key"),
|
||||
nil,
|
||||
)
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
key, ok = rawKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
log.Warn().Msg("Received public key is not rsa")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
"/errors/invalid-auth-signature",
|
||||
"invalid authorization signature",
|
||||
other.IntoPointer("Received public key is not rsa"),
|
||||
nil,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
hash := sha256.Sum256([]byte(stringToCheck))
|
||||
err = rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], []byte(signature))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Signature verification failed")
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusUnauthorized,
|
||||
"/errors/invalid-auth-signature",
|
||||
"invalid authorization signature",
|
||||
other.IntoPointer(
|
||||
"Verification of the given signature with the user's public key failed",
|
||||
),
|
||||
nil,
|
||||
)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildStringToCheck(r *http.Request, rawSignedHeaders string) string {
|
||||
headersToUse := strings.Split(rawSignedHeaders, " ")
|
||||
// Doesn't matter here if convert failed as path doesn't have prefix in that case
|
||||
path, ok := r.Context().Value((FullPathContextKey)).(string)
|
||||
if !ok {
|
||||
path = r.URL.Path
|
||||
}
|
||||
return webshared.GenerateStringToSign(
|
||||
r.Method,
|
||||
config.GlobalConfig.General.GetFullDomain(),
|
||||
path,
|
||||
r.Header,
|
||||
headersToUse,
|
||||
)
|
||||
}
|
||||
|
||||
func getRequestingActor(keyId string) (*models.User, error) {
|
||||
// Cut away key id to get AP url
|
||||
keyId = strings.Split(keyId, "#")[0]
|
||||
acc, err := dbgen.User.GetRemoteAccountByApUrl(keyId)
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
acc, err = activitypub.ImportRemoteAccountByAPUrl(keyId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return acc, nil
|
||||
case nil:
|
||||
return acc, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import (
|
|||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/web/public/api"
|
||||
webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
@ -61,6 +62,7 @@ func New(addr string, duckImg *string) *Server {
|
|||
Handler: webutils.ChainMiddlewares(
|
||||
handler,
|
||||
webutils.BuildLoggingMiddleware(map[string]string{"server": "public"}),
|
||||
webmiddleware.AppendFullPathMiddleware,
|
||||
),
|
||||
Addr: addr,
|
||||
}
|
||||
|
|
|
@ -43,14 +43,14 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte
|
|||
var signedString string
|
||||
var usedHeaders []string
|
||||
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
||||
tmp, tmp2, err := CreateSignatureED(method, r.URL, mappedHeaders, privateKeyBytes)
|
||||
tmp, tmp2, err := CreateSignatureED(method, r.URL, headers, privateKeyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedString = tmp
|
||||
usedHeaders = tmp2
|
||||
} else {
|
||||
tmp, tmp2, err := CreateSignatureRSA(method, r.URL, mappedHeaders, privateKeyBytes)
|
||||
tmp, tmp2, err := CreateSignatureRSA(method, r.URL, headers, privateKeyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -2,9 +2,11 @@ package webshared
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/maputils"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
|
@ -17,7 +19,7 @@ import (
|
|||
func CreateSignatureRSA(
|
||||
method string,
|
||||
target *url.URL,
|
||||
headers map[string]string,
|
||||
headers http.Header,
|
||||
privateKeyBytes []byte,
|
||||
) (string, []string, error) {
|
||||
message, usedHeaders := genPreSignatureString(method, target, headers)
|
||||
|
@ -42,7 +44,7 @@ func CreateSignatureRSA(
|
|||
func CreateSignatureED(
|
||||
method string,
|
||||
target *url.URL,
|
||||
headers map[string]string,
|
||||
headers http.Header,
|
||||
privateKeyBytes []byte,
|
||||
) (string, []string, error) {
|
||||
message, usedHeaders := genPreSignatureString(method, target, headers)
|
||||
|
@ -56,24 +58,37 @@ func CreateSignatureED(
|
|||
func genPreSignatureString(
|
||||
method string,
|
||||
target *url.URL,
|
||||
headers map[string]string,
|
||||
headers http.Header,
|
||||
) (string, []string) {
|
||||
dataBuilder := strings.Builder{}
|
||||
dataBuilder.WriteString("(request-target): ")
|
||||
dataBuilder.WriteString(strings.ToLower(method) + " ")
|
||||
dataBuilder.WriteString(target.Path + "\n")
|
||||
dataBuilder.WriteString("host: ")
|
||||
dataBuilder.WriteString(target.Host + "\n")
|
||||
// dataBuilder.WriteString("algorithm: rsa-sha256\n")
|
||||
// usedHeaders := []string{"(request-target)", "algorithm"}
|
||||
usedHeaders := []string{"(request-target)", "host"}
|
||||
for k, v := range headers {
|
||||
dataBuilder.WriteString(k + ": " + v + "\n")
|
||||
usedHeaders = append(usedHeaders, k)
|
||||
usedHeaders = append(usedHeaders, maputils.KeysFromMap(headers)...)
|
||||
return GenerateStringToSign(method, target.Host, target.Path, headers, usedHeaders), usedHeaders
|
||||
}
|
||||
|
||||
func GenerateStringToSign(
|
||||
method string,
|
||||
host string,
|
||||
path string,
|
||||
headers http.Header,
|
||||
headerOrder []string,
|
||||
) string {
|
||||
dataBuilder := strings.Builder{}
|
||||
for _, v := range headerOrder {
|
||||
v = strings.ToLower(v)
|
||||
switch v {
|
||||
case "(request-target)":
|
||||
dataBuilder.WriteString(v + ": " + strings.ToLower(method) + " " + path + "\n")
|
||||
case "host":
|
||||
dataBuilder.WriteString(v + ": " + host + "\n")
|
||||
default:
|
||||
dataBuilder.WriteString(v + ": " + headers.Get(v) + "\n")
|
||||
}
|
||||
// dataBuilder.WriteString(k + ": " + v + "\n")
|
||||
// usedHeaders = append(usedHeaders, k)
|
||||
}
|
||||
tmp := strings.TrimSuffix(dataBuilder.String(), "\n")
|
||||
log.Debug().Str("Raw signature string", tmp).Send()
|
||||
return tmp, usedHeaders
|
||||
return tmp
|
||||
}
|
||||
|
||||
// Generate the content of the "Signature" header based on
|
||||
|
|
Loading…
Reference in a new issue