Signing works
This commit is contained in:
parent
d272fa90b4
commit
da2a89010c
19 changed files with 348 additions and 100 deletions
|
@ -4,10 +4,9 @@ import (
|
|||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httputils "git.mstar.dev/mstar/goutils/http"
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
@ -27,15 +26,28 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
|||
data := Inbound{}
|
||||
err := dec.Decode(&data)
|
||||
if err != nil {
|
||||
httputils.HttpErr(w, 0, "json decode failed", http.StatusBadRequest)
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
"/errors/bad-request-data",
|
||||
"bad request data",
|
||||
nil,
|
||||
map[string]any{
|
||||
"sample": Inbound{
|
||||
Username: "bob",
|
||||
Content: "Heya there, this is sample data",
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
user, err := dbgen.User.GetByUsername(data.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
httputils.HttpErr(w, 0, "no user with that name", http.StatusNotFound)
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||
} else {
|
||||
log.Error().Err(err).Str("name", data.Username).Msg("Failed to find user")
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -69,13 +81,13 @@ func notesFrom(w http.ResponseWriter, r *http.Request) {
|
|||
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)
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
notes, err := dbgen.Note.GetNotesPaged(user.ID, 0, uint8(models.NOTE_TARGET_PUBLIC))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("name", username).Msg("Failed to get notes")
|
||||
httputils.HttpErr(w, 0, "failed to get notes", http.StatusInternalServerError)
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
publicNotes := sliceutils.Map(notes, func(t models.Note) webshared.Note {
|
||||
|
@ -83,11 +95,5 @@ func notesFrom(w http.ResponseWriter, r *http.Request) {
|
|||
n.FromModel(&t)
|
||||
return n
|
||||
})
|
||||
jsonNotes, err := json.Marshal(publicNotes)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to marshal notes")
|
||||
httputils.HttpErr(w, 0, "failed to marshal", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, string(jsonNotes))
|
||||
webutils.SendJson(w, publicNotes)
|
||||
}
|
||||
|
|
|
@ -2,19 +2,18 @@ package webdebug
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httputils "git.mstar.dev/mstar/goutils/http"
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||
|
@ -27,25 +26,27 @@ func getNonDeletedUsers(w http.ResponseWriter, r *http.Request) {
|
|||
var err error
|
||||
page, err = strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
httputils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest)
|
||||
webutils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
users, err := dbgen.User.GetPagedAllNonDeleted(uint(page))
|
||||
if err != nil {
|
||||
httputils.HttpErr(w, 0, "failed to get users", http.StatusInternalServerError)
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusInternalServerError,
|
||||
"/errors/db-failure",
|
||||
"database failure",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
return
|
||||
}
|
||||
marshalled, err := json.Marshal(sliceutils.Map(users, func(t models.User) webshared.User {
|
||||
webutils.SendJson(w, sliceutils.Map(users, func(t models.User) webshared.User {
|
||||
u := webshared.User{}
|
||||
u.FromModel(&t)
|
||||
return u
|
||||
}))
|
||||
if err != nil {
|
||||
httputils.HttpErr(w, 0, "failed to marshal users", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, string(marshalled))
|
||||
}
|
||||
|
||||
func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -61,19 +62,43 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
|||
data := Inbound{}
|
||||
err := jsonDecoder.Decode(&data)
|
||||
if err != nil {
|
||||
httputils.HttpErr(w, 0, "decode failed", http.StatusBadRequest)
|
||||
webutils.ProblemDetails(
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
"/errors/bad-request-data",
|
||||
"bad request data",
|
||||
nil,
|
||||
map[string]any{
|
||||
"sample": Inbound{
|
||||
Username: "bob",
|
||||
Displayname: "Bob Bobbington",
|
||||
Description: "Bobbing Bobs bop to Bobs bobbing beats",
|
||||
Birthday: other.IntoPointer(time.Now()),
|
||||
Location: nil,
|
||||
IsBot: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// publicKey, privateKey, err := ed25519.GenerateKey(nil)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
|
||||
publicKeyEdBytes, privateKeyEdBytes, err := shared.GenerateKeypair(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate and marshal public key")
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
publicKeyRsaBytes, privateKeyRsaBytes, err := shared.GenerateKeypair(false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate and marshal public key")
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pkeyId := make([]byte, 64)
|
||||
_, err = rand.Read(pkeyId)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate passkey id")
|
||||
httputils.HttpErr(w, 0, "failed to generate passkey id", http.StatusInternalServerError)
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -84,8 +109,10 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
|||
u.Description,
|
||||
u.IsBot,
|
||||
u.ServerId,
|
||||
u.PrivateKey,
|
||||
u.PublicKey,
|
||||
u.PrivateKeyEd,
|
||||
u.PublicKeyEd,
|
||||
u.PrivateKeyRsa,
|
||||
u.PublicKeyRsa,
|
||||
u.PasskeyId,
|
||||
)
|
||||
if data.Birthday != nil {
|
||||
|
@ -95,14 +122,16 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
|||
query = query.Select(u.Location)
|
||||
}
|
||||
user := models.User{
|
||||
Username: data.Username,
|
||||
DisplayName: data.Displayname,
|
||||
Description: data.Description,
|
||||
IsBot: data.IsBot,
|
||||
ServerId: 1, // Hardcoded, Self is always first ID
|
||||
PublicKey: publicKeyBytes,
|
||||
PrivateKey: privateKeyBytes,
|
||||
PasskeyId: pkeyId,
|
||||
Username: data.Username,
|
||||
DisplayName: data.Displayname,
|
||||
Description: data.Description,
|
||||
IsBot: data.IsBot,
|
||||
ServerId: 1, // Hardcoded, Self is always first ID
|
||||
PublicKeyRsa: publicKeyRsaBytes,
|
||||
PublicKeyEd: publicKeyEdBytes,
|
||||
PrivateKeyRsa: privateKeyRsaBytes,
|
||||
PrivateKeyEd: privateKeyEdBytes,
|
||||
PasskeyId: pkeyId,
|
||||
}
|
||||
if data.Birthday != nil {
|
||||
user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday}
|
||||
|
@ -112,7 +141,7 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
if err = u.Create(&user); err != nil {
|
||||
log.Error().Err(err).Msg("failed to create new local user")
|
||||
httputils.HttpErr(w, 0, "db failure", http.StatusInternalServerError)
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
func BuildActivitypubRouter() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("/user/{id}", users)
|
||||
router.HandleFunc("/user/{id}/inbox", userInbox)
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "in ap")
|
||||
})
|
||||
|
|
|
@ -3,13 +3,17 @@ package activitypub
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||
)
|
||||
|
||||
var baseLdContext = []any{
|
||||
|
@ -23,17 +27,30 @@ func users(w http.ResponseWriter, r *http.Request) {
|
|||
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"`
|
||||
type OutboundMedia struct {
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
MediaType string `json:"mediaType"`
|
||||
}
|
||||
type Outbound struct {
|
||||
Context []any `json:"@context"`
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
PreferredUsername string `json:"preferredUsername"`
|
||||
Inbox string `json:"inbox"`
|
||||
PublicKey OutboundKey `json:"publicKey"`
|
||||
Published time.Time `json:"published"`
|
||||
DisplayName string `json:"name"`
|
||||
Description *string `json:"summary,omitempty"`
|
||||
PublicUrl string `json:"url"`
|
||||
Icon *OutboundMedia `json:"icon,omitempty"`
|
||||
Banner *OutboundMedia `json:"image,omitempty"`
|
||||
}
|
||||
log := hlog.FromRequest(r)
|
||||
userId := r.PathValue("id")
|
||||
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).First()
|
||||
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).
|
||||
Preload(dbgen.User.Icon).Preload(dbgen.User.Banner).
|
||||
First()
|
||||
if err != nil {
|
||||
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
|
||||
if storage.HandleReconnectError(err) {
|
||||
|
@ -43,20 +60,47 @@ func users(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
return
|
||||
}
|
||||
// fmt.Println(x509.ParsePKCS1PublicKey(user.PublicKey))
|
||||
|
||||
apUrl := userIdToApUrl(user.ID)
|
||||
var keyBytes string
|
||||
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
||||
keyBytes = keyBytesToPem(user.PublicKeyEd)
|
||||
} else {
|
||||
keyBytes = keyBytesToPem(user.PublicKeyRsa)
|
||||
}
|
||||
data := Outbound{
|
||||
Context: baseLdContext,
|
||||
Id: apUrl,
|
||||
Type: "Person",
|
||||
PreferredUsername: user.DisplayName,
|
||||
PreferredUsername: user.Username,
|
||||
Inbox: apUrl + "/inbox",
|
||||
// PublicKey: OutboundKey{
|
||||
// Id: apUrl + "#main-key",
|
||||
// Owner: apUrl,
|
||||
// Pem: keyBytesToPem(user.PublicKey),
|
||||
// },
|
||||
PublicKey: OutboundKey{
|
||||
Id: apUrl + "#main-key",
|
||||
Owner: apUrl,
|
||||
Pem: keyBytes,
|
||||
},
|
||||
Published: user.CreatedAt,
|
||||
DisplayName: user.DisplayName,
|
||||
PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username,
|
||||
}
|
||||
if user.Description != "" {
|
||||
data.Description = &user.Description
|
||||
}
|
||||
if user.Icon != nil {
|
||||
log.Debug().Msg("icon found")
|
||||
data.Icon = &OutboundMedia{
|
||||
Type: "Image",
|
||||
Url: webshared.EnsurePublicUrl(user.Icon.Location),
|
||||
MediaType: user.Icon.Type,
|
||||
}
|
||||
}
|
||||
if user.Banner != nil {
|
||||
log.Debug().Msg("icon banner")
|
||||
data.Banner = &OutboundMedia{
|
||||
Type: "Image",
|
||||
Url: webshared.EnsurePublicUrl(user.Banner.Location),
|
||||
MediaType: user.Banner.Type,
|
||||
}
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(data)
|
||||
|
@ -64,6 +108,13 @@ func users(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Fprint(w, string(encoded))
|
||||
}
|
||||
|
||||
func userInbox(w http.ResponseWriter, r *http.Request) {
|
||||
log := hlog.FromRequest(r)
|
||||
userId := r.PathValue("id")
|
||||
data, err := io.ReadAll(r.Body)
|
||||
log.Info().Err(err).Str("userId", userId).Bytes("body", data).Msg("Inbox message")
|
||||
}
|
||||
|
||||
/*
|
||||
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
|
||||
Fuck you.
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
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",
|
||||
"bad-request-data": "The data provided in the request doesn't match the requirements, see problem details' detail field for more information",
|
||||
}
|
||||
|
||||
func errorTypeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -40,7 +40,7 @@ type Server struct {
|
|||
server *http.Server
|
||||
}
|
||||
|
||||
func New(addr string) *Server {
|
||||
func New(addr string, duckImg *string) *Server {
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
|
@ -51,6 +51,11 @@ func New(addr string) *Server {
|
|||
handler.HandleFunc("GET /.well-known/nodeinfo", api.WellKnownNodeinfo)
|
||||
handler.HandleFunc("GET /nodeinfo/2.1", api.Nodeinfo)
|
||||
handler.HandleFunc("GET /errors/{name}", errorTypeHandler)
|
||||
handler.HandleFunc("GET /default-image", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "image/web")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"duck.webp\"")
|
||||
fmt.Fprint(w, *duckImg)
|
||||
})
|
||||
server := http.Server{
|
||||
Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware),
|
||||
Addr: addr,
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"slices"
|
||||
"time"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
)
|
||||
|
@ -97,7 +98,11 @@ func (u *User) FromModel(m *models.User) {
|
|||
u.BannerId = &m.IconId.String
|
||||
}
|
||||
u.Indexable = m.Indexable
|
||||
u.PublicKey = append(u.PublicKey, m.PublicKey...)
|
||||
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
||||
u.PublicKey = append(u.PublicKey, m.PublicKeyEd...)
|
||||
} else {
|
||||
u.PublicKey = append(u.PublicKey, m.PublicKeyRsa...)
|
||||
}
|
||||
u.RestrictedFollow = m.RestrictedFollow
|
||||
if m.Location.Valid {
|
||||
u.Location = &m.Location.String
|
||||
|
|
16
web/shared/linstromUrlType.go
Normal file
16
web/shared/linstromUrlType.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package webshared
|
||||
|
||||
import "strings"
|
||||
|
||||
// TODO: Define linstrom uri type
|
||||
|
||||
var hardcodedUrls = map[string]string{
|
||||
"default-media": "/default-image",
|
||||
}
|
||||
|
||||
func EnsurePublicUrl(rawUrl string) string {
|
||||
if !strings.HasPrefix(rawUrl, "linstrom://") {
|
||||
return rawUrl
|
||||
}
|
||||
return strings.Replace(rawUrl, "linstrom://", "/", 1)
|
||||
}
|
40
web/shared/signing.go
Normal file
40
web/shared/signing.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package webshared
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
func CreateSignatureRSA(
|
||||
method string,
|
||||
target string,
|
||||
headers map[string]string,
|
||||
privateKeyBytes []byte,
|
||||
) (string, error) {
|
||||
message := genPreSignatureString(method, target, headers)
|
||||
signed, err := shared.Sign(message, privateKeyBytes, true)
|
||||
return string(signed), err
|
||||
}
|
||||
|
||||
func CreateSignatureED(
|
||||
method string,
|
||||
target string,
|
||||
headers map[string]string,
|
||||
privateKeyBytes []byte,
|
||||
) (string, error) {
|
||||
message := genPreSignatureString(method, target, headers)
|
||||
signed, err := shared.Sign(message, privateKeyBytes, false)
|
||||
return string(signed), err
|
||||
}
|
||||
|
||||
func genPreSignatureString(method, target string, headers map[string]string) string {
|
||||
dataBuilder := strings.Builder{}
|
||||
dataBuilder.WriteString("(request-target) ")
|
||||
dataBuilder.WriteString(strings.ToLower(method) + " ")
|
||||
dataBuilder.WriteString(target + "\n")
|
||||
for k, v := range headers {
|
||||
dataBuilder.WriteString(k + ": " + v + "\n")
|
||||
}
|
||||
return dataBuilder.String()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue