Merge branch 'feature/webfinger' into develop
This commit is contained in:
commit
68be711d04
|
@ -32,6 +32,8 @@ import (
|
||||||
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
|
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
|
||||||
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
|
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
|
||||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||||
|
webfingerhttpdelivery "source.toby3d.me/toby3d/home/internal/webfinger/delivery/http"
|
||||||
|
webfingerucase "source.toby3d.me/toby3d/home/internal/webfinger/usecase"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
@ -58,7 +60,21 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
||||||
entries := pagefsrepo.NewFileSystemPageRepository(contentDir)
|
entries := pagefsrepo.NewFileSystemPageRepository(contentDir)
|
||||||
entrier := pageucase.NewEntryUseCase(entries, resources)
|
entrier := pageucase.NewEntryUseCase(entries, resources)
|
||||||
serverer := servercase.NewServerUseCase(sites)
|
serverer := servercase.NewServerUseCase(sites)
|
||||||
|
webfingerer := webfingerucase.NewWebFingerUseCase(sites)
|
||||||
|
webfingerHandler := webfingerhttpdelivery.NewHandler(webfingerer)
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
head, tail := urlutil.ShiftPath(r.URL.Path)
|
||||||
|
|
||||||
|
switch head {
|
||||||
|
case ".well-known":
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(tail, "/webfinger"):
|
||||||
|
webfingerHandler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE(toby3d): any static file is public and unprotected by
|
// NOTE(toby3d): any static file is public and unprotected by
|
||||||
// design, so it's safe to search it first before deep down to
|
// design, so it's safe to search it first before deep down to
|
||||||
// any page or it's resource which might be protected by
|
// any page or it's resource which might be protected by
|
||||||
|
@ -83,8 +99,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
||||||
if s.IsMultiLingual() {
|
if s.IsMultiLingual() {
|
||||||
// NOTE(toby3d): $HOME_CONTENT_DIR contains at least two
|
// NOTE(toby3d): $HOME_CONTENT_DIR contains at least two
|
||||||
// index.md with different language codes.
|
// index.md with different language codes.
|
||||||
head, tail := urlutil.ShiftPath(r.URL.Path)
|
if head, tail = urlutil.ShiftPath(r.URL.Path); head == "" {
|
||||||
if head == "" {
|
|
||||||
// NOTE(toby3d): client request just '/', try to
|
// NOTE(toby3d): client request just '/', try to
|
||||||
// understand which language subdirectory is
|
// understand which language subdirectory is
|
||||||
// need to redirect.
|
// need to redirect.
|
||||||
|
@ -198,12 +213,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
||||||
})
|
})
|
||||||
chain := middleware.Chain{
|
chain := middleware.Chain{
|
||||||
middleware.LogFmt(),
|
middleware.LogFmt(),
|
||||||
middleware.Redirect(middleware.RedirectConfig{
|
middleware.Redirect(middleware.RedirectConfig{Serverer: serverer}),
|
||||||
Serverer: serverer,
|
middleware.Header(middleware.HeaderConfig{Serverer: serverer}),
|
||||||
}),
|
|
||||||
middleware.Header(middleware.HeaderConfig{
|
|
||||||
Serverer: serverer,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &App{server: &http.Server{
|
return &App{server: &http.Server{
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HeaderAcceptLanguage string = "Accept-Language"
|
HeaderAccept string = "Accept"
|
||||||
HeaderAuthorization string = "Authorization"
|
HeaderAcceptLanguage string = "Accept-Language"
|
||||||
HeaderContentLanguage string = "Content-Language"
|
HeaderAuthorization string = "Authorization"
|
||||||
HeaderContentType string = "Content-Type"
|
HeaderContentLanguage string = "Content-Language"
|
||||||
HeaderCookie string = "Cookie"
|
HeaderContentType string = "Content-Type"
|
||||||
|
HeaderCookie string = "Cookie"
|
||||||
|
HeaderAccessControlAllowOrigin string = "Access-Control-Allow-Origin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MIMETextHTML string = "text/html"
|
MIMEApplicationActivityJSON string = "application/activity+json"
|
||||||
MIMETextHTMLCharsetUTF8 string = MIMETextHTML + "; " + charsetUTF8
|
MIMEApplicationActivityJSONCharsetUTF8 string = MIMEApplicationActivityJSON + "; " + charsetUTF8
|
||||||
MIMETextPlain string = "text/plain"
|
MIMEApplicationJRDKJSON string = "application/jrd+json"
|
||||||
MIMETextPlainCharsetUTF8 string = MIMETextPlain + "; " + charsetUTF8
|
MIMEApplicationJRDKJSONCharsetUTF8 string = MIMEApplicationJRDKJSON + "; " + charsetUTF8
|
||||||
|
MIMEApplicationJSON string = "application/json"
|
||||||
|
MIMEApplicationJSONCharsetUTF8 string = MIMEApplicationJSON + "; " + charsetUTF8
|
||||||
|
MIMEApplicationLdJSON string = "application/ld+json"
|
||||||
|
MIMEApplicationLdJSONProfile string = MIMEApplicationLdJSON + "; " + profile
|
||||||
|
MIMETextHTML string = "text/html"
|
||||||
|
MIMETextHTMLCharsetUTF8 string = MIMETextHTML + "; " + charsetUTF8
|
||||||
|
MIMETextPlain string = "text/plain"
|
||||||
|
MIMETextPlainCharsetUTF8 string = MIMETextPlain + "; " + charsetUTF8
|
||||||
)
|
)
|
||||||
|
|
||||||
const Und string = "und"
|
const Und string = "und"
|
||||||
|
|
||||||
const charsetUTF8 string = "charset=UTF-8"
|
const (
|
||||||
|
charsetUTF8 string = "charset=UTF-8"
|
||||||
|
profile string = `profile="https://www.w3.org/ns/activitystreams"`
|
||||||
|
)
|
||||||
|
|
|
@ -13,6 +13,8 @@ type Config struct {
|
||||||
Host string `env:"HOST" envDefault:"0.0.0.0"`
|
Host string `env:"HOST" envDefault:"0.0.0.0"`
|
||||||
ThemeDir string `env:"THEME_DIR" envDefault:"theme"`
|
ThemeDir string `env:"THEME_DIR" envDefault:"theme"`
|
||||||
StaticDir string `env:"STATIC_DIR" envDefault:"static"`
|
StaticDir string `env:"STATIC_DIR" envDefault:"static"`
|
||||||
|
CertKey string `env:"CERT_KEY"`
|
||||||
|
CertFile string `env:"CERT_FILE"`
|
||||||
Port uint16 `env:"PORT" envDefault:"3000"`
|
Port uint16 `env:"PORT" envDefault:"3000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"image"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -42,11 +43,34 @@ func TestSite(tb testing.TB) *Site {
|
||||||
DefaultLanguage: en,
|
DefaultLanguage: en,
|
||||||
Language: ru,
|
Language: ru,
|
||||||
Languages: []Language{en, ru},
|
Languages: []Language{en, ru},
|
||||||
BaseURL: &url.URL{Scheme: "http", Host: "127.0.0.1:3000", Path: "/"},
|
BaseURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/"},
|
||||||
TimeZone: time.UTC,
|
TimeZone: time.UTC,
|
||||||
File: NewPath(filepath.Join("content", "index.en.md")),
|
File: NewPath(filepath.Join("content", "index.en.md")),
|
||||||
Title: "Testing",
|
Title: "Testing",
|
||||||
Resources: make([]*Resource, 0),
|
Resources: []*Resource{
|
||||||
|
{
|
||||||
|
modTime: time.Now().UTC().Add(-1 * time.Hour),
|
||||||
|
params: make(map[string]any),
|
||||||
|
File: NewPath("photo.png"),
|
||||||
|
mediaType: NewMediaType("image/png"),
|
||||||
|
key: "photo",
|
||||||
|
name: "photo",
|
||||||
|
resourceType: ResourceTypeImage,
|
||||||
|
title: "",
|
||||||
|
image: image.Config{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modTime: time.Now().UTC().Add(-2 * time.Hour),
|
||||||
|
params: make(map[string]any),
|
||||||
|
File: NewPath("logo.jpg"),
|
||||||
|
mediaType: NewMediaType("image/jpeg"),
|
||||||
|
key: "logo",
|
||||||
|
name: "logo",
|
||||||
|
resourceType: ResourceTypeImage,
|
||||||
|
title: "",
|
||||||
|
image: image.Config{},
|
||||||
|
},
|
||||||
|
},
|
||||||
Params: map[string]any{
|
Params: map[string]any{
|
||||||
"server": map[string]any{
|
"server": map[string]any{
|
||||||
"headers": []any{map[string]any{
|
"headers": []any{map[string]any{
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package stub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/home/internal/domain"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/site"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubSiteRepository struct {
|
||||||
|
err error
|
||||||
|
site *domain.Site
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStubSiteRepository(site *domain.Site, err error) site.Repository {
|
||||||
|
return &stubSiteRepository{
|
||||||
|
site: site,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *stubSiteRepository) Get(_ context.Context, _ domain.Language) (*domain.Site, error) {
|
||||||
|
return repo.site, repo.err
|
||||||
|
}
|
|
@ -9,10 +9,17 @@ import (
|
||||||
"source.toby3d.me/toby3d/home/internal/site"
|
"source.toby3d.me/toby3d/home/internal/site"
|
||||||
)
|
)
|
||||||
|
|
||||||
type siteUseCase struct {
|
type (
|
||||||
sites site.Repository
|
siteUseCase struct {
|
||||||
resources resource.Repository
|
sites site.Repository
|
||||||
}
|
resources resource.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
stubSiteUseCase struct {
|
||||||
|
err error
|
||||||
|
site *domain.Site
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func NewSiteUseCase(sites site.Repository, resources resource.Repository) site.UseCase {
|
func NewSiteUseCase(sites site.Repository, resources resource.Repository) site.UseCase {
|
||||||
return &siteUseCase{
|
return &siteUseCase{
|
||||||
|
@ -50,3 +57,14 @@ func (ucase *siteUseCase) Do(ctx context.Context, lang domain.Language) (*domain
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewStubSiteUseCase(site *domain.Site, err error) site.UseCase {
|
||||||
|
return &stubSiteUseCase{
|
||||||
|
site: site,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ucase *stubSiteUseCase) Do(_ context.Context, _ domain.Language) (*domain.Site, error) {
|
||||||
|
return ucase.site, ucase.err
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"subject":"acct:user@example.com","links":[{"rel":"self","type":"application/activity+json","href":"https://example.com/"},{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://example.com/"},{"rel":"http://webfinger.net/rel/avatar","type":"image/jpeg","href":"https://example.com/logo.jpg"}]}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/home/internal/common"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/domain"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/webfinger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Handler struct {
|
||||||
|
webFingers webfinger.UseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
Request struct {
|
||||||
|
Resource *Resource
|
||||||
|
Rel string // optional
|
||||||
|
}
|
||||||
|
|
||||||
|
Response struct {
|
||||||
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
|
Links []Link `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Link struct {
|
||||||
|
Titles map[string]string `json:"titles,omitempty"`
|
||||||
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Href string `json:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource struct {
|
||||||
|
User string
|
||||||
|
Host string
|
||||||
|
IsEmail bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrResource error = errors.New("'resource' URL must be absolute, or an email address")
|
||||||
|
|
||||||
|
func NewHandler(webFingers webfinger.UseCase) *Handler {
|
||||||
|
return &Handler{webFingers: webFingers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set(common.HeaderAccessControlAllowOrigin, "*")
|
||||||
|
|
||||||
|
if r.TLS == nil {
|
||||||
|
http.Error(w, "webfinger usage is https only", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method != "" && r.Method != http.MethodGet {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(Request)
|
||||||
|
if err := req.bind(r.URL.Query()); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
site, err := h.webFingers.Do(r.Context(), strings.TrimPrefix(req.Resource.String(), "acct:"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := NewResponse(req.Resource.String())
|
||||||
|
// TODO(toby3d): fill resp.Aliases with profile syndications links.
|
||||||
|
// NOTE(toby3d): rel="self" link is a Mastodon requirement for
|
||||||
|
// ActivityPub support.
|
||||||
|
resp.Links = append(resp.Links, Link{
|
||||||
|
Titles: make(map[string]string),
|
||||||
|
Properties: make(map[string]string),
|
||||||
|
Rel: "self",
|
||||||
|
Type: common.MIMEApplicationActivityJSON,
|
||||||
|
Href: site.BaseURL.String(),
|
||||||
|
}, Link{
|
||||||
|
Titles: make(map[string]string),
|
||||||
|
Properties: make(map[string]string),
|
||||||
|
Rel: "http://webfinger.net/rel/profile-page",
|
||||||
|
Type: "text/html",
|
||||||
|
Href: site.BaseURL.String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO(toby3d): explicitly control which resource is to be used.
|
||||||
|
if photo := site.Resources.GetType(domain.ResourceTypeImage).GetMatch("logo"); photo != nil {
|
||||||
|
resp.Links = append(resp.Links, Link{
|
||||||
|
Titles: make(map[string]string),
|
||||||
|
Properties: make(map[string]string),
|
||||||
|
Rel: "http://webfinger.net/rel/avatar",
|
||||||
|
Type: photo.MediaType().String(),
|
||||||
|
Href: site.BaseURL.JoinPath(photo.File.Path()).String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJRDKJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) bind(req url.Values) error {
|
||||||
|
if !req.Has("resource") {
|
||||||
|
return ErrResource
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if r.Resource, err = ParseResource(req.Get("resource")); err != nil {
|
||||||
|
return fmt.Errorf("cannot parse 'resource' query as URI: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse(subject string) *Response {
|
||||||
|
return &Response{
|
||||||
|
Properties: make(map[string]string),
|
||||||
|
Subject: subject,
|
||||||
|
Aliases: make([]string, 0),
|
||||||
|
Links: make([]Link, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseResource(raw string) (*Resource, error) {
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse 'resource' query value as URL or email address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
switch {
|
||||||
|
case u.Path != "":
|
||||||
|
parts = strings.Split(u.Path, "@")
|
||||||
|
case u.Opaque != "":
|
||||||
|
parts = strings.Split(u.Opaque, "@")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Resource{
|
||||||
|
IsEmail: u.Scheme != "acct",
|
||||||
|
User: parts[0],
|
||||||
|
Host: parts[len(parts)-1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Resource) String() string {
|
||||||
|
if !r.IsEmail {
|
||||||
|
return "acct:" + r.User + "@" + r.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.User + "@" + r.Host
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package http_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/home/internal/common"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/domain"
|
||||||
|
sitestubrepo "source.toby3d.me/toby3d/home/internal/site/repository/stub"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/testutil"
|
||||||
|
delivery "source.toby3d.me/toby3d/home/internal/webfinger/delivery/http"
|
||||||
|
webfingerucase "source.toby3d.me/toby3d/home/internal/webfinger/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_ServeHTTP(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testSite := domain.TestSite(t)
|
||||||
|
u := testSite.BaseURL.JoinPath(".well-known", "webfinger")
|
||||||
|
u.RawQuery = "resource=acct:user@" + testSite.BaseURL.Hostname()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
req.Header.Set(common.HeaderAccept, common.MIMEApplicationJRDKJSON+", "+common.MIMEApplicationJSON)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
delivery.NewHandler(webfingerucase.NewWebFingerUseCase(sitestubrepo.NewStubSiteRepository(testSite, nil))).
|
||||||
|
ServeHTTP(w, req)
|
||||||
|
testutil.GoldenEqual(t, w.Result().Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseResource(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for name, tc := range map[string]struct {
|
||||||
|
input string
|
||||||
|
expect delivery.Resource
|
||||||
|
}{
|
||||||
|
"profile": {"acct:user@example.com", delivery.Resource{"user", "example.com", false}},
|
||||||
|
"email": {"user@example.com", delivery.Resource{"user", "example.com", true}},
|
||||||
|
} {
|
||||||
|
name, tc := name, tc
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
result, err := delivery.ParseResource(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.expect, *result); diff != "" {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package webfinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/home/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UseCase interface {
|
||||||
|
Do(ctx context.Context, acct string) (*domain.Site, error)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/home/internal/domain"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/site"
|
||||||
|
"source.toby3d.me/toby3d/home/internal/webfinger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webFingerUseCase struct {
|
||||||
|
sites site.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebFingerUseCase(sites site.Repository) webfinger.UseCase {
|
||||||
|
return &webFingerUseCase{sites: sites}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ucase *webFingerUseCase) Do(ctx context.Context, acct string) (*domain.Site, error) {
|
||||||
|
parts := strings.Split(acct, "@")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid 'acct' value format, got %s, want [user, host]", parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
site, err := ucase.sites.Get(ctx, domain.LanguageUnd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read global site config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(parts[1], site.BaseURL.Hostname()) {
|
||||||
|
return nil, fmt.Errorf("requested %s user outside %s resource", parts[1], site.BaseURL.Hostname())
|
||||||
|
}
|
||||||
|
|
||||||
|
return site, nil
|
||||||
|
}
|
18
main.go
18
main.go
|
@ -6,9 +6,11 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -40,6 +42,20 @@ func main() {
|
||||||
logger.Fatalln("cannot unmarshal configuration into domain:", err)
|
logger.Fatalln("cannot unmarshal configuration into domain:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", config.AddrPort().String())
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalln("cannot listen requested address:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CertFile != "" && config.CertKey != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(config.CertFile, config.CertKey)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalln("cannot load certificate files from config:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln = tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||||
|
}
|
||||||
|
|
||||||
for _, dir := range []*string{
|
for _, dir := range []*string{
|
||||||
&config.ContentDir,
|
&config.ContentDir,
|
||||||
&config.ThemeDir,
|
&config.ThemeDir,
|
||||||
|
@ -90,7 +106,7 @@ func main() {
|
||||||
go func() {
|
go func() {
|
||||||
logger.Printf("starting server on %d...", config.Port)
|
logger.Printf("starting server on %d...", config.Port)
|
||||||
|
|
||||||
if err = app.Run(nil); err != nil {
|
if err = app.Run(ln); err != nil {
|
||||||
logger.Fatalln("cannot run app:", err)
|
logger.Fatalln("cannot run app:", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
Loading…
Reference in New Issue