GoBlog/webmentionVerification.go

211 lines
5.4 KiB
Go
Raw Normal View History

package main
import (
"bytes"
2021-05-14 16:24:02 +00:00
"context"
"database/sql"
"errors"
"fmt"
"io"
2020-11-25 11:36:14 +00:00
"log"
"net/http"
2021-05-14 16:24:02 +00:00
"net/http/httptest"
"net/url"
2020-11-25 11:36:14 +00:00
"os"
2021-02-27 07:31:06 +00:00
"strings"
2020-11-16 13:18:14 +00:00
"github.com/PuerkitoBio/goquery"
2020-11-25 11:36:14 +00:00
"github.com/joncrlsn/dque"
2021-04-16 18:00:38 +00:00
"github.com/thoas/go-funk"
"willnorris.com/go/microformats"
)
2020-11-25 11:36:14 +00:00
var wmQueue *dque.DQue
func wmMentionBuilder() interface{} {
return &mention{}
}
func initWebmentionQueue() (err error) {
queuePath := "queues"
if _, err := os.Stat(queuePath); os.IsNotExist(err) {
2021-02-08 17:51:07 +00:00
if err := os.Mkdir(queuePath, 0755); err != nil {
return err
}
} else if err != nil {
return err
2020-11-25 11:36:14 +00:00
}
wmQueue, err = dque.NewOrOpen("webmention", queuePath, 5, wmMentionBuilder)
if err != nil {
return err
}
startWebmentionQueue()
return nil
}
func startWebmentionQueue() {
go func() {
for {
2020-11-25 11:36:14 +00:00
if i, err := wmQueue.PeekBlock(); err == nil {
if i == nil {
// Empty request
_, _ = wmQueue.Dequeue()
continue
}
if m, ok := i.(*mention); ok {
err = m.verifyMention()
if err != nil {
log.Println(fmt.Sprintf("Failed to verify webmention from %s to %s: %s", m.Source, m.Target, err.Error()))
}
_, _ = wmQueue.Dequeue()
} else {
// Invalid type
_, _ = wmQueue.Dequeue()
}
}
}
}()
}
2020-11-25 11:36:14 +00:00
func queueMention(m *mention) error {
if wm := appConfig.Webmention; wm != nil && wm.DisableReceiving {
return errors.New("webmention receiving disabled")
}
2020-11-25 11:36:14 +00:00
return wmQueue.Enqueue(m)
}
func (m *mention) verifyMention() error {
req, err := http.NewRequest(http.MethodGet, m.Source, nil)
if err != nil {
return err
}
2021-05-14 16:24:02 +00:00
var resp *http.Response
2021-02-27 07:31:06 +00:00
if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) {
2021-05-14 16:24:02 +00:00
rec := httptest.NewRecorder()
d.ServeHTTP(rec, req.WithContext(context.WithValue(req.Context(), loggedInKey, true)))
resp = rec.Result()
} else {
req.Header.Set(userAgent, appUserAgent)
resp, err = appHttpClient.Do(req)
if err != nil {
return err
}
}
2020-11-25 11:36:14 +00:00
err = m.verifyReader(resp.Body)
_ = resp.Body.Close()
if err != nil {
_, err := appDbExec("delete from webmentions where source = @source and target = @target", sql.Named("source", m.Source), sql.Named("target", m.Target))
return err
}
if len(m.Content) > 500 {
m.Content = m.Content[0:497] + "…"
}
2020-12-13 09:39:00 +00:00
if len(m.Title) > 60 {
m.Title = m.Title[0:57] + "…"
}
newStatus := webmentionStatusVerified
2020-11-25 11:36:14 +00:00
if webmentionExists(m.Source, m.Target) {
_, err = appDbExec("update webmentions set status = @status, title = @title, content = @content, author = @author where source = @source and target = @target",
sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author), sql.Named("source", m.Source), sql.Named("target", m.Target))
} else {
_, err = appDbExec("insert into webmentions (source, target, created, status, title, content, author) values (@source, @target, @created, @status, @title, @content, @author)",
sql.Named("source", m.Source), sql.Named("target", m.Target), sql.Named("created", m.Created), sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author))
sendNotification(fmt.Sprintf("New webmention from %s to %s", m.Source, m.Target))
}
return err
}
2020-11-25 11:36:14 +00:00
func (m *mention) verifyReader(body io.Reader) error {
2020-11-16 13:18:14 +00:00
var linksBuffer, gqBuffer, mfBuffer bytes.Buffer
2021-02-08 17:51:07 +00:00
if _, err := io.Copy(io.MultiWriter(&linksBuffer, &gqBuffer, &mfBuffer), body); err != nil {
return err
}
2020-11-16 13:18:14 +00:00
// Check if source mentions target
links, err := allLinksFromHTML(&linksBuffer, m.Source)
if err != nil {
return err
}
2021-04-16 18:00:38 +00:00
if _, hasLink := funk.FindString(links, func(s string) bool {
return unescapedPath(s) == unescapedPath(m.Target)
}); !hasLink {
2020-11-16 13:18:14 +00:00
return errors.New("target not found in source")
}
2020-11-16 13:18:14 +00:00
// Set title
doc, err := goquery.NewDocumentFromReader(&gqBuffer)
if err != nil {
return err
}
if title := doc.Find("title"); title != nil {
m.Title = title.Text()
}
// Fill mention attributes
sourceURL, err := url.Parse(m.Source)
if err != nil {
return err
}
2020-11-25 11:36:14 +00:00
m.fillFromData(microformats.Parse(&mfBuffer, sourceURL))
return nil
}
2020-11-25 11:36:14 +00:00
func (m *mention) fillFromData(mf *microformats.Data) {
for _, i := range mf.Items {
2020-11-25 11:36:14 +00:00
m.fill(i)
}
}
2020-11-25 11:36:14 +00:00
func (m *mention) fill(mf *microformats.Microformat) bool {
if mfHasType(mf, "h-entry") {
// Check URL
if url, ok := mf.Properties["url"]; ok && len(url) > 0 {
if url0, ok := url[0].(string); ok {
if url0 != m.Source {
// Not correct URL
return false
}
}
}
// Title
if name, ok := mf.Properties["name"]; ok && len(name) > 0 {
if title, ok := name[0].(string); ok {
m.Title = strings.TrimSpace(title)
}
}
// Content
if contents, ok := mf.Properties["content"]; ok && len(contents) > 0 {
if content, ok := contents[0].(map[string]string); ok {
if contentValue, ok := content["value"]; ok {
m.Content = strings.TrimSpace(contentValue)
}
}
}
// Author
if authors, ok := mf.Properties["author"]; ok && len(authors) > 0 {
if author, ok := authors[0].(*microformats.Microformat); ok {
if names, ok := author.Properties["name"]; ok && len(names) > 0 {
2020-11-16 13:18:14 +00:00
if name, ok := names[0].(string); ok {
m.Author = strings.TrimSpace(name)
}
}
}
}
return true
}
if len(mf.Children) > 0 {
2020-11-16 13:18:14 +00:00
for _, mfc := range mf.Children {
2020-11-25 11:36:14 +00:00
if m.fill(mfc) {
return true
}
}
}
return false
}
func mfHasType(mf *microformats.Microformat, typ string) bool {
for _, t := range mf.Type {
if typ == t {
return true
}
}
return false
}