Browse Source

Big refactoring: Avoid global vars almost everywhere

master
Jan-Lukas Else 6 months ago
parent
commit
9714d65679
  1. 42
      .vscode/tasks.json
  2. 186
      activityPub.go
  3. 26
      activityPubSending.go
  4. 47
      activityStreams.go
  5. 73
      app.go
  6. 60
      authentication.go
  7. 59
      blogroll.go
  8. 50
      blogstats.go
  9. 64
      cache.go
  10. 22
      captcha.go
  11. 86
      check.go
  12. 54
      comments.go
  13. 23
      commentsAdmin.go
  14. 51
      config.go
  15. 10
      customPages.go
  16. 108
      database.go
  17. 4
      databaseMigrations.go
  18. 47
      editor.go
  19. 12
      errors.go
  20. 22
      feeds.go
  21. 12
      go.mod
  22. 19
      go.sum
  23. 8
      healthcheck.go
  24. 56
      hooks.go
  25. 414
      http.go
  26. 16
      httpLogs.go
  27. 6
      indieAuth.go
  28. 86
      indieAuthServer.go
  29. 45
      main.go
  30. 35
      markdown.go
  31. 4
      media.go
  32. 12
      mediaCompression.go
  33. 208
      micropub.go
  34. 38
      micropubMedia.go
  35. 33
      minify.go
  36. 10
      nodeinfo.go
  37. 43
      notifications.go
  38. 18
      persistentCache.go
  39. 14
      postAliases.go
  40. 65
      posts.go
  41. 179
      postsDb.go
  42. 30
      postsFuncs.go
  43. 16
      queue.go
  44. 12
      regexRedirects.go
  45. 63
      render.go
  46. 10
      reverseGeo.go
  47. 4
      robotstxt.go
  48. 12
      search.go
  49. 29
      sessions.go
  50. 6
      shortDomain.go
  51. 22
      shortPath.go
  52. 40
      sitemap.go
  53. 4
      staticFiles.go
  54. 10
      taxonomies.go
  55. 21
      telegram.go
  56. 38
      templateAssets.go
  57. 8
      templateStrings.go
  58. 12
      tor.go
  59. 54
      webmention.go
  60. 41
      webmentionAdmin.go
  61. 24
      webmentionSending.go
  62. 36
      webmentionVerification.go

42
.vscode/tasks.json

@ -1,15 +1,33 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"command": "go build --tags \"libsqlite3 linux sqlite_fts5\"",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"command": "go build",
"options": {
"env": {
"GOFLAGS": "-tags=linux,libsqlite3,sqlite_fts5"
}
},
"group": {
"kind": "build",
"isDefault": true
}
]
}
},
{
"label": "Test",
"type": "shell",
"command": "go test",
"options": {
"env": {
"GOFLAGS": "-tags=linux,libsqlite3,sqlite_fts5"
}
},
"group": {
"kind": "test",
"isDefault": true
}
}
]
}

186
activityPub.go

@ -1,7 +1,6 @@
package main
import (
"crypto/rsa"
"crypto/x509"
"database/sql"
"encoding/json"
@ -14,50 +13,41 @@ import (
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-fed/httpsig"
)
var (
apPrivateKey *rsa.PrivateKey
apPostSigner httpsig.Signer
apPostSignMutex *sync.Mutex = &sync.Mutex{}
webfingerResources map[string]*configBlog
webfingerAccts map[string]string
)
func initActivityPub() error {
if !appConfig.ActivityPub.Enabled {
func (a *goBlog) initActivityPub() error {
if !a.cfg.ActivityPub.Enabled {
return nil
}
// Add hooks
postPostHooks = append(postPostHooks, func(p *post) {
a.pPostHooks = append(a.pPostHooks, func(p *post) {
if p.isPublishedSectionPost() {
p.apPost()
a.apPost(p)
}
})
postUpdateHooks = append(postUpdateHooks, func(p *post) {
a.pUpdateHooks = append(a.pUpdateHooks, func(p *post) {
if p.isPublishedSectionPost() {
p.apUpdate()
a.apUpdate(p)
}
})
postDeleteHooks = append(postDeleteHooks, func(p *post) {
p.apDelete()
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
a.apDelete(p)
})
// Prepare webfinger
webfingerResources = map[string]*configBlog{}
webfingerAccts = map[string]string{}
for name, blog := range appConfig.Blogs {
acct := "acct:" + name + "@" + appConfig.Server.publicHostname
webfingerResources[acct] = blog
webfingerResources[blog.apIri()] = blog
webfingerAccts[blog.apIri()] = acct
a.webfingerResources = map[string]*configBlog{}
a.webfingerAccts = map[string]string{}
for name, blog := range a.cfg.Blogs {
acct := "acct:" + name + "@" + a.cfg.Server.publicHostname
a.webfingerResources[acct] = blog
a.webfingerResources[a.apIri(blog)] = blog
a.webfingerAccts[a.apIri(blog)] = acct
}
// Read key and prepare signing
pkfile, err := os.ReadFile(appConfig.ActivityPub.KeyPath)
pkfile, err := os.ReadFile(a.cfg.ActivityPub.KeyPath)
if err != nil {
return err
}
@ -65,11 +55,11 @@ func initActivityPub() error {
if privateKeyDecoded == nil {
return errors.New("failed to decode private key")
}
apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
a.apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
if err != nil {
return err
}
apPostSigner, _, err = httpsig.NewSigner(
a.apPostSigner, _, err = httpsig.NewSigner(
[]httpsig.Algorithm{httpsig.RSA_SHA256},
httpsig.DigestSha256,
[]string{httpsig.RequestTarget, "date", "host", "digest"},
@ -80,32 +70,32 @@ func initActivityPub() error {
return err
}
// Init send queue
initAPSendQueue()
a.initAPSendQueue()
return nil
}
func apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
blog, ok := webfingerResources[r.URL.Query().Get("resource")]
func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
if !ok {
serveError(w, r, "Resource not found", http.StatusNotFound)
a.serveError(w, r, "Resource not found", http.StatusNotFound)
return
}
b, _ := json.Marshal(map[string]interface{}{
"subject": webfingerAccts[blog.apIri()],
"subject": a.webfingerAccts[a.apIri(blog)],
"aliases": []string{
webfingerAccts[blog.apIri()],
blog.apIri(),
a.webfingerAccts[a.apIri(blog)],
a.apIri(blog),
},
"links": []map[string]string{
{
"rel": "self",
"type": contentTypeAS,
"href": blog.apIri(),
"href": a.apIri(blog),
},
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": blog.apIri(),
"href": a.apIri(blog),
},
},
})
@ -113,19 +103,19 @@ func apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
_, _ = writeMinified(w, contentTypeJSON, b)
}
func apHandleInbox(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
blogName := chi.URLParam(r, "blog")
blog := appConfig.Blogs[blogName]
blog := a.cfg.Blogs[blogName]
if blog == nil {
serveError(w, r, "Inbox not found", http.StatusNotFound)
a.serveError(w, r, "Inbox not found", http.StatusNotFound)
return
}
blogIri := blog.apIri()
blogIri := a.apIri(blog)
// Verify request
requestActor, requestKey, requestActorStatus, err := apVerifySignature(r)
if err != nil {
// Send 401 because signature could not be verified
serveError(w, r, err.Error(), http.StatusUnauthorized)
a.serveError(w, r, err.Error(), http.StatusUnauthorized)
return
}
if requestActorStatus != 0 {
@ -134,12 +124,12 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
if err == nil {
u.Fragment = ""
u.RawFragment = ""
_ = apRemoveFollower(blogName, u.String())
_ = a.db.apRemoveFollower(blogName, u.String())
w.WriteHeader(http.StatusOK)
return
}
}
serveError(w, r, "Error when trying to get request actor", http.StatusBadRequest)
a.serveError(w, r, "Error when trying to get request actor", http.StatusBadRequest)
return
}
// Parse activity
@ -147,29 +137,29 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
err = json.NewDecoder(r.Body).Decode(&activity)
_ = r.Body.Close()
if err != nil {
serveError(w, r, "Failed to decode body", http.StatusBadRequest)
a.serveError(w, r, "Failed to decode body", http.StatusBadRequest)
return
}
// Get and check activity actor
activityActor, ok := activity["actor"].(string)
if !ok {
serveError(w, r, "actor in activity is no string", http.StatusBadRequest)
a.serveError(w, r, "actor in activity is no string", http.StatusBadRequest)
return
}
if activityActor != requestActor.ID {
serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
a.serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
return
}
// Do
switch activity["type"] {
case "Follow":
apAccept(blogName, blog, activity)
a.apAccept(blogName, blog, activity)
case "Undo":
{
if object, ok := activity["object"].(map[string]interface{}); ok {
if objectType, ok := object["type"].(string); ok && objectType == "Follow" {
if iri, ok := object["actor"].(string); ok && iri == activityActor {
_ = apRemoveFollower(blogName, activityActor)
_ = a.db.apRemoveFollower(blogName, activityActor)
}
}
}
@ -181,13 +171,13 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
id, hasID := object["id"].(string)
if hasReplyToString && hasID && len(inReplyTo) > 0 && len(id) > 0 && strings.Contains(inReplyTo, blogIri) {
// It's an ActivityPub reply; save reply as webmention
_ = createWebmention(id, inReplyTo)
_ = a.createWebmention(id, inReplyTo)
} else if content, hasContent := object["content"].(string); hasContent && hasID && len(id) > 0 {
// May be a mention; find links to blog and save them as webmentions
if links, err := allLinksFromHTML(strings.NewReader(content), id); err == nil {
for _, link := range links {
if strings.Contains(link, blogIri) {
_ = createWebmention(id, link)
_ = a.createWebmention(id, link)
}
}
}
@ -198,21 +188,21 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
case "Block":
{
if object, ok := activity["object"].(string); ok && len(object) > 0 && object == activityActor {
_ = apRemoveFollower(blogName, activityActor)
_ = a.db.apRemoveFollower(blogName, activityActor)
}
}
case "Like":
{
likeObject, likeObjectOk := activity["object"].(string)
if likeObjectOk && len(likeObject) > 0 && strings.Contains(likeObject, blogIri) {
sendNotification(fmt.Sprintf("%s liked %s", activityActor, likeObject))
a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, likeObject))
}
}
case "Announce":
{
announceObject, announceObjectOk := activity["object"].(string)
if announceObjectOk && len(announceObject) > 0 && strings.Contains(announceObject, blogIri) {
sendNotification(fmt.Sprintf("%s announced %s", activityActor, announceObject))
a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, announceObject))
}
}
}
@ -277,8 +267,8 @@ func apGetRemoteActor(iri string) (*asPerson, int, error) {
return actor, 0, nil
}
func apGetAllInboxes(blog string) ([]string, error) {
rows, err := appDb.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
func (db *database) apGetAllInboxes(blog string) ([]string, error) {
rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
if err != nil {
return nil, err
}
@ -294,27 +284,27 @@ func apGetAllInboxes(blog string) ([]string, error) {
return inboxes, nil
}
func apAddFollower(blog, follower, inbox string) error {
_, err := appDb.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
func (db *database) apAddFollower(blog, follower, inbox string) error {
_, err := db.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
return err
}
func apRemoveFollower(blog, follower string) error {
_, err := appDb.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
func (db *database) apRemoveFollower(blog, follower string) error {
_, err := db.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
return err
}
func apRemoveInbox(inbox string) error {
_, err := appDb.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
func (db *database) apRemoveInbox(inbox string) error {
_, err := db.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
return err
}
func (p *post) apPost() {
n := p.toASNote()
apSendToAllFollowers(p.Blog, map[string]interface{}{
func (a *goBlog) apPost(p *post) {
n := a.toASNote(p)
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"actor": appConfig.Blogs[p.Blog].apIri(),
"id": p.fullURL(),
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p),
"published": n.Published,
"type": "Create",
"object": n,
@ -322,46 +312,46 @@ func (p *post) apPost() {
if n.InReplyTo != "" {
// Is reply, so announce it
time.Sleep(30 * time.Second)
p.apAnnounce()
a.apAnnounce(p)
}
}
func (p *post) apUpdate() {
apSendToAllFollowers(p.Blog, map[string]interface{}{
func (a *goBlog) apUpdate(p *post) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"actor": appConfig.Blogs[p.Blog].apIri(),
"id": p.fullURL(),
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p),
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
"type": "Update",
"object": p.toASNote(),
"object": a.toASNote(p),
})
}
func (p *post) apAnnounce() {
apSendToAllFollowers(p.Blog, map[string]interface{}{
func (a *goBlog) apAnnounce(p *post) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"actor": appConfig.Blogs[p.Blog].apIri(),
"id": p.fullURL() + "#announce",
"published": p.toASNote().Published,
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p) + "#announce",
"published": a.toASNote(p).Published,
"type": "Announce",
"object": p.fullURL(),
"object": a.fullPostURL(p),
})
}
func (p *post) apDelete() {
apSendToAllFollowers(p.Blog, map[string]interface{}{
func (a *goBlog) apDelete(p *post) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"actor": appConfig.Blogs[p.Blog].apIri(),
"id": p.fullURL() + "#delete",
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p) + "#delete",
"type": "Delete",
"object": map[string]string{
"id": p.fullURL(),
"id": a.fullPostURL(p),
"type": "Tombstone",
},
})
}
func apAccept(blogName string, blog *configBlog, follow map[string]interface{}) {
func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]interface{}) {
// it's a follow, write it down
newFollower := follow["actor"].(string)
log.Println("New follow request:", newFollower)
@ -381,7 +371,7 @@ func apAccept(blogName string, blog *configBlog, follow map[string]interface{})
if endpoints := follower.Endpoints; endpoints != nil && endpoints.SharedInbox != "" {
inbox = endpoints.SharedInbox
}
if err = apAddFollower(blogName, follower.ID, inbox); err != nil {
if err = a.db.apAddFollower(blogName, follower.ID, inbox); err != nil {
return
}
// remove @context from the inner activity
@ -389,37 +379,37 @@ func apAccept(blogName string, blog *configBlog, follow map[string]interface{})
accept := map[string]interface{}{
"@context": asContext,
"to": follow["actor"],
"actor": blog.apIri(),
"actor": a.apIri(blog),
"object": follow,
"type": "Accept",
}
_, accept["id"] = apNewID(blog)
_ = apQueueSendSigned(blog.apIri(), follower.Inbox, accept)
_, accept["id"] = a.apNewID(blog)
_ = a.db.apQueueSendSigned(a.apIri(blog), follower.Inbox, accept)
}
func apSendToAllFollowers(blog string, activity interface{}) {
inboxes, err := apGetAllInboxes(blog)
func (a *goBlog) apSendToAllFollowers(blog string, activity interface{}) {
inboxes, err := a.db.apGetAllInboxes(blog)
if err != nil {
log.Println("Failed to retrieve inboxes:", err.Error())
return
}
apSendTo(appConfig.Blogs[blog].apIri(), activity, inboxes)
a.db.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes)
}
func apSendTo(blogIri string, activity interface{}, inboxes []string) {
func (db *database) apSendTo(blogIri string, activity interface{}, inboxes []string) {
for _, i := range inboxes {
go func(inbox string) {
_ = apQueueSendSigned(blogIri, inbox, activity)
_ = db.apQueueSendSigned(blogIri, inbox, activity)
}(i)
}
}
func apNewID(blog *configBlog) (hash string, url string) {
return hash, blog.apIri() + generateRandomString(16)
func (a *goBlog) apNewID(blog *configBlog) (hash string, url string) {
return hash, a.apIri(blog) + generateRandomString(16)
}
func (b *configBlog) apIri() string {
return appConfig.Server.PublicAddress + b.Path
func (a *goBlog) apIri(b *configBlog) string {
return a.cfg.Server.PublicAddress + b.Path
}
func apRequestIsSuccess(code int) bool {

26
activityPubSending.go

@ -19,10 +19,10 @@ type apRequest struct {
Try int
}
func initAPSendQueue() {
func (a *goBlog) initAPSendQueue() {
go func() {
for {
qi, err := peekQueue("ap")
qi, err := a.db.peekQueue("ap")
if err != nil {
log.Println(err.Error())
continue
@ -31,22 +31,22 @@ func initAPSendQueue() {
err = gob.NewDecoder(bytes.NewReader(qi.content)).Decode(&r)
if err != nil {
log.Println(err.Error())
_ = qi.dequeue()
_ = a.db.dequeue(qi)
continue
}
if err := apSendSigned(r.BlogIri, r.To, r.Activity); err != nil {
if err := a.apSendSigned(r.BlogIri, r.To, r.Activity); err != nil {
if r.Try++; r.Try < 20 {
// Try it again
qi.content, _ = r.encode()
_ = qi.reschedule(time.Duration(r.Try) * 10 * time.Minute)
_ = a.db.reschedule(qi, time.Duration(r.Try)*10*time.Minute)
continue
} else {
log.Printf("Request to %s failed for the 20th time", r.To)
log.Println()
_ = apRemoveInbox(r.To)
_ = a.db.apRemoveInbox(r.To)
}
}
err = qi.dequeue()
err = a.db.dequeue(qi)
if err != nil {
log.Println(err.Error())
}
@ -58,7 +58,7 @@ func initAPSendQueue() {
}()
}
func apQueueSendSigned(blogIri, to string, activity interface{}) error {
func (db *database) apQueueSendSigned(blogIri, to string, activity interface{}) error {
body, err := json.Marshal(activity)
if err != nil {
return err
@ -71,7 +71,7 @@ func apQueueSendSigned(blogIri, to string, activity interface{}) error {
if err != nil {
return err
}
return enqueue("ap", b, time.Now())
return db.enqueue("ap", b, time.Now())
}
func (r *apRequest) encode() ([]byte, error) {
@ -83,7 +83,7 @@ func (r *apRequest) encode() ([]byte, error) {
return buf.Bytes(), nil
}
func apSendSigned(blogIri, to string, activity []byte) error {
func (a *goBlog) apSendSigned(blogIri, to string, activity []byte) error {
// Create request context with timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
@ -105,9 +105,9 @@ func apSendSigned(blogIri, to string, activity []byte) error {
r.Header.Set(contentType, contentTypeASUTF8)
r.Header.Set("Host", iri.Host)
// Sign request
apPostSignMutex.Lock()
err = apPostSigner.SignRequest(apPrivateKey, blogIri+"#main-key", r, activity)
apPostSignMutex.Unlock()
a.apPostSignMutex.Lock()
err = a.apPostSigner.SignRequest(a.apPrivateKey, blogIri+"#main-key", r, activity)
a.apPostSignMutex.Unlock()
if err != nil {
return err
}

47
activityStreams.go

@ -22,9 +22,9 @@ var asCheckMediaTypes = []contenttype.MediaType{
const asRequestKey requestContextKey = "asRequest"
func checkActivityStreamsRequest(next http.Handler) http.Handler {
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if ap := appConfig.ActivityPub; ap != nil && ap.Enabled {
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
// Check if accepted media type is not HTML
if mt, _, err := contenttype.GetAcceptableMediaType(r, asCheckMediaTypes); err == nil && mt.String() != asCheckMediaTypes[0].String() {
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))
@ -87,21 +87,21 @@ type asEndpoints struct {
SharedInbox string `json:"sharedInbox,omitempty"`
}
func (p *post) serveActivityStreams(w http.ResponseWriter) {
b, _ := json.Marshal(p.toASNote())
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
b, _ := json.Marshal(a.toASNote(p))
w.Header().Set(contentType, contentTypeASUTF8)
_, _ = writeMinified(w, contentTypeAS, b)
}
func (p *post) toASNote() *asNote {
func (a *goBlog) toASNote(p *post) *asNote {
// Create a Note object
as := &asNote{
Context: asContext,
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
MediaType: contentTypeHTML,
ID: p.fullURL(),
URL: p.fullURL(),
AttributedTo: appConfig.Blogs[p.Blog].apIri(),
ID: a.fullPostURL(p),
URL: a.fullPostURL(p),
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
}
// Name and Type
if title := p.title(); title != "" {
@ -111,9 +111,9 @@ func (p *post) toASNote() *asNote {
as.Type = "Note"
}
// Content
as.Content = string(p.absoluteHTML())
as.Content = string(a.absoluteHTML(p))
// Attachments
if images := p.Parameters[appConfig.Micropub.PhotoParam]; len(images) > 0 {
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
for _, image := range images {
as.Attachment = append(as.Attachment, &asAttachment{
Type: "Image",
@ -122,12 +122,12 @@ func (p *post) toASNote() *asNote {
}
}
// Tags
for _, tagTax := range appConfig.ActivityPub.TagsTaxonomies {
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
for _, tag := range p.Parameters[tagTax] {
as.Tag = append(as.Tag, &asTag{
Type: "Hashtag",
Name: tag,
Href: appConfig.Server.PublicAddress + appConfig.Blogs[p.Blog].getRelativePath(fmt.Sprintf("/%s/%s", tagTax, urlize(tag))),
Href: a.cfg.Server.PublicAddress + a.cfg.Blogs[p.Blog].getRelativePath(fmt.Sprintf("/%s/%s", tagTax, urlize(tag))),
})
}
}
@ -144,30 +144,31 @@ func (p *post) toASNote() *asNote {
}
}
// Reply
if replyLink := p.firstParameter(appConfig.Micropub.ReplyParam); replyLink != "" {
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
as.InReplyTo = replyLink
}
return as
}
func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
publicKeyDer, err := x509.MarshalPKIXPublicKey(&apPrivateKey.PublicKey)
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
b := a.cfg.Blogs[blog]
publicKeyDer, err := x509.MarshalPKIXPublicKey(&(a.apPrivateKey.PublicKey))
if err != nil {
serveError(w, r, "Failed to marshal public key", http.StatusInternalServerError)
a.serveError(w, r, "Failed to marshal public key", http.StatusInternalServerError)
return
}
asBlog := &asPerson{
Context: asContext,
Type: "Person",
ID: b.apIri(),
URL: b.apIri(),
ID: a.apIri(b),
URL: a.apIri(b),
Name: b.Title,
Summary: b.Description,
PreferredUsername: blog,
Inbox: appConfig.Server.PublicAddress + "/activitypub/inbox/" + blog,
Inbox: a.cfg.Server.PublicAddress + "/activitypub/inbox/" + blog,
PublicKey: &asPublicKey{
Owner: b.apIri(),
ID: b.apIri() + "#main-key",
Owner: a.apIri(b),
ID: a.apIri(b) + "#main-key",
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
@ -176,10 +177,10 @@ func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter, r
},
}
// Add profile picture
if appConfig.User.Picture != "" {
if a.cfg.User.Picture != "" {
asBlog.Icon = &asAttachment{
Type: "Image",
URL: appConfig.User.Picture,
URL: a.cfg.User.Picture,
}
}
jb, _ := json.Marshal(asBlog)

73
app.go

@ -0,0 +1,73 @@
package main
import (
"crypto/rsa"
"html/template"
"net/http"
"sync"
ts "git.jlel.se/jlelse/template-strings"
"github.com/go-chi/chi/v5"
"github.com/go-fed/httpsig"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/yuin/goldmark"
"golang.org/x/sync/singleflight"
)
type goBlog struct {
// ActivityPub
apPrivateKey *rsa.PrivateKey
apPostSigner httpsig.Signer
apPostSignMutex sync.Mutex
webfingerResources map[string]*configBlog
webfingerAccts map[string]string
// Assets
assetFileNames map[string]string
assetFiles map[string]*assetFile
// Blogroll
blogrollCacheGroup singleflight.Group
// Cache
cache *cache
// Config
cfg *config
// Database
db *database
// Hooks
pPostHooks []postHookFunc
pUpdateHooks []postHookFunc
pDeleteHooks []postHookFunc
// HTTP
d *dynamicHandler
privateMode bool
privateModeHandler []func(http.Handler) http.Handler
captchaHandler http.Handler
micropubRouter *chi.Mux
indieAuthRouter *chi.Mux
webmentionsRouter *chi.Mux
notificationsRouter *chi.Mux
activitypubRouter *chi.Mux
editorRouter *chi.Mux
commentsRouter *chi.Mux
searchRouter *chi.Mux
setBlogMiddlewares map[string]func(http.Handler) http.Handler
sectionMiddlewares map[string]func(http.Handler) http.Handler
taxonomyMiddlewares map[string]func(http.Handler) http.Handler
photosMiddlewares map[string]func(http.Handler) http.Handler
searchMiddlewares map[string]func(http.Handler) http.Handler
customPagesMiddlewares map[string]func(http.Handler) http.Handler
commentsMiddlewares map[string]func(http.Handler) http.Handler
// Logs
logf *rotatelogs.RotateLogs
// Markdown
md, absoluteMd goldmark.Markdown
// Regex Redirects
regexRedirects []*regexRedirect
// Rendering
templates map[string]*template.Template
// Sessions
loginSessions, captchaSessions *dbSessionStore
// Template strings
ts *ts.TemplateStrings
// Tor
torAddress string
}

60
authentication.go

@ -11,14 +11,14 @@ import (
"github.com/pquerna/otp/totp"
)
func checkCredentials(username, password, totpPasscode string) bool {
return username == appConfig.User.Nick &&
password == appConfig.User.Password &&
(appConfig.User.TOTP == "" || totp.Validate(totpPasscode, appConfig.User.TOTP))
func (a *goBlog) checkCredentials(username, password, totpPasscode string) bool {
return username == a.cfg.User.Nick &&
password == a.cfg.User.Password &&
(a.cfg.User.TOTP == "" || totp.Validate(totpPasscode, a.cfg.User.TOTP))
}
func checkAppPasswords(username, password string) bool {
for _, apw := range appConfig.User.AppPasswords {
func (a *goBlog) checkAppPasswords(username, password string) bool {
for _, apw := range a.cfg.User.AppPasswords {
if apw.Username == username && apw.Password == password {
return true
}
@ -26,11 +26,11 @@ func checkAppPasswords(username, password string) bool {
return false
}
func jwtKey() []byte {
return []byte(appConfig.Server.JWTSecret)
func (a *goBlog) jwtKey() []byte {
return []byte(a.cfg.Server.JWTSecret)
}
func authMiddleware(next http.Handler) http.Handler {
func (a *goBlog) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Check if already logged in
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
@ -38,12 +38,12 @@ func authMiddleware(next http.Handler) http.Handler {
return
}
// 2. Check BasicAuth (just for app passwords)
if username, password, ok := r.BasicAuth(); ok && checkAppPasswords(username, password) {
if username, password, ok := r.BasicAuth(); ok && a.checkAppPasswords(username, password) {
next.ServeHTTP(w, r)
return
}
// 3. Check login cookie
if checkLoginCookie(r) {
if a.checkLoginCookie(r) {
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
return
}
@ -57,12 +57,12 @@ func authMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm()
b = []byte(r.PostForm.Encode())
}
render(w, r, templateLogin, &renderData{
a.render(w, r, templateLogin, &renderData{
Data: map[string]interface{}{
"loginmethod": r.Method,
"loginheaders": base64.StdEncoding.EncodeToString(h),
"loginbody": base64.StdEncoding.EncodeToString(b),
"totp": appConfig.User.TOTP != "",
"totp": a.cfg.User.TOTP != "",
},
})
})
@ -70,9 +70,9 @@ func authMiddleware(next http.Handler) http.Handler {
const loggedInKey requestContextKey = "loggedIn"
func checkLoggedIn(next http.Handler) http.Handler {
func (a *goBlog) checkLoggedIn(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if checkLoginCookie(r) {
if a.checkLoginCookie(r) {
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
return
}
@ -80,8 +80,8 @@ func checkLoggedIn(next http.Handler) http.Handler {
})
}
func checkLoginCookie(r *http.Request) bool {
ses, err := loginSessionsStore.Get(r, "l")
func (a *goBlog) checkLoginCookie(r *http.Request) bool {
ses, err := a.loginSessions.Get(r, "l")
if err == nil && ses != nil {
if login, ok := ses.Values["login"]; ok && login.(bool) {
return true
@ -90,15 +90,15 @@ func checkLoginCookie(r *http.Request) bool {
return false
}
func checkIsLogin(next http.Handler) http.Handler {
func (a *goBlog) checkIsLogin(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if !checkLogin(rw, r) {
if !a.checkLogin(rw, r) {
next.ServeHTTP(rw, r)
}
})
}
func checkLogin(w http.ResponseWriter, r *http.Request) bool {
func (a *goBlog) checkLogin(w http.ResponseWriter, r *http.Request) bool {
if r.Method != http.MethodPost {
return false
}
@ -109,8 +109,8 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
return false
}
// Check credential
if !checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
if !a.checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
a.serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
return true
}
// Prepare original request
@ -124,20 +124,20 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
req.Header[k] = v
}
// Cookie
ses, err := loginSessionsStore.Get(r, "l")
ses, err := a.loginSessions.Get(r, "l")
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return true
}
ses.Values["login"] = true
cookie, err := loginSessionsStore.SaveGetCookie(r, w, ses)
cookie, err := a.loginSessions.SaveGetCookie(r, w, ses)
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return true
}
req.AddCookie(cookie)
// Serve original request
d.ServeHTTP(w, req)
a.d.ServeHTTP(w, req)
return true
}
@ -146,9 +146,9 @@ func serveLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
}
func serveLogout(w http.ResponseWriter, r *http.Request) {
if ses, err := loginSessionsStore.Get(r, "l"); err == nil && ses != nil {
_ = loginSessionsStore.Delete(r, w, ses)
func (a *goBlog) serveLogout(w http.ResponseWriter, r *http.Request) {
if ses, err := a.loginSessions.Get(r, "l"); err == nil && ses != nil {
_ = a.loginSessions.Delete(r, w, ses)
}
http.Redirect(w, r, "/", http.StatusFound)
}

59
blogroll.go

@ -13,28 +13,25 @@ import (
"github.com/kaorimatz/go-opml"
servertiming "github.com/mitchellh/go-server-timing"
"github.com/thoas/go-funk"
"golang.org/x/sync/singleflight"
)
var blogrollCacheGroup singleflight.Group
func serveBlogroll(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
t := servertiming.FromContext(r.Context()).NewMetric("bg").Start()
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return getBlogrollOutlines(blog)
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return a.getBlogrollOutlines(blog)
})
t.Stop()
if err != nil {
log.Println("Failed to get outlines:", err.Error())
serveError(w, r, "", http.StatusInternalServerError)
log.Printf("Failed to get outlines: %v", err)
a.serveError(w, r, "", http.StatusInternalServerError)
return
}
if appConfig.Cache != nil && appConfig.Cache.Enable {
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
}
c := appConfig.Blogs[blog].Blogroll
render(w, r, templateBlogroll, &renderData{
c := a.cfg.Blogs[blog].Blogroll
a.render(w, r, templateBlogroll, &renderData{
BlogString: blog,
Data: map[string]interface{}{
"Title": c.Title,
@ -45,34 +42,32 @@ func serveBlogroll(w http.ResponseWriter, r *http.Request) {
})
}
func serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return getBlogrollOutlines(blog)
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return a.getBlogrollOutlines(blog)
})
if err != nil {
log.Println("Failed to get outlines:", err.Error())
serveError(w, r, "", http.StatusInternalServerError)
log.Printf("Failed to get outlines: %v", err)
a.serveError(w, r, "", http.StatusInternalServerError)
return
}
if appConfig.Cache != nil && appConfig.Cache.Enable {
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
}
w.Header().Set(contentType, contentTypeXMLUTF8)
mw := minifier.Writer(contentTypeXML, w)
defer func() {
_ = mw.Close()
}()
_ = opml.Render(mw, &opml.OPML{
var opmlBytes bytes.Buffer
_ = opml.Render(&opmlBytes, &opml.OPML{
Version: "2.0",
DateCreated: time.Now().UTC(),
Outlines: outlines.([]*opml.Outline),
})
_, _ = writeMinified(w, contentTypeXML, opmlBytes.Bytes())
}
func getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
config := appConfig.Blogs[blog].Blogroll
if cache := loadOutlineCache(blog); cache != nil {
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
config := a.cfg.Blogs[blog].Blogroll
if cache := a.db.loadOutlineCache(blog); cache != nil {
return cache, nil
}
req, err := http.NewRequest(http.MethodGet, config.Opml, nil)
@ -112,22 +107,22 @@ func getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
} else {
outlines = sortOutlines(outlines)
}
cacheOutlines(blog, outlines)
a.db.cacheOutlines(blog, outlines)
return outlines, nil
}
func cacheOutlines(blog string, outlines []*opml.Outline) {
func (db *database) cacheOutlines(blog string, outlines []*opml.Outline) {
var opmlBuffer bytes.Buffer
_ = opml.Render(&opmlBuffer, &opml.OPML{
Version: "2.0",
DateCreated: time.Now().UTC(),
Outlines: outlines,
})
_ = cachePersistently("blogroll_"+blog, opmlBuffer.Bytes())
_ = db.cachePersistently("blogroll_"+blog, opmlBuffer.Bytes())
}
func loadOutlineCache(blog string) []*opml.Outline {
data, err := retrievePersistentCache("blogroll_" + blog)
func (db *database) loadOutlineCache(blog string) []*opml.Outline {
data, err := db.retrievePersistentCache("blogroll_" + blog)
if err != nil || data == nil {
return nil
}

50
blogstats.go

@ -9,19 +9,19 @@ import (
"golang.org/x/sync/singleflight"
)
func initBlogStats() {
func (a *goBlog) initBlogStats() {
f := func(p *post) {
resetBlogStats(p.Blog)
a.db.resetBlogStats(p.Blog)
}
postPostHooks = append(postPostHooks, f)
postUpdateHooks = append(postUpdateHooks, f)
postDeleteHooks = append(postDeleteHooks, f)
a.pPostHooks = append(a.pPostHooks, f)
a.pUpdateHooks = append(a.pUpdateHooks, f)
a.pDeleteHooks = append(a.pDeleteHooks, f)
}
func serveBlogStats(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
canonical := blogPath(blog) + appConfig.Blogs[blog].BlogStats.Path
render(w, r, templateBlogStats, &renderData{
canonical := a.blogPath(blog) + a.cfg.Blogs[blog].BlogStats.Path
a.render(w, r, templateBlogStats, &renderData{
BlogString: blog,
Canonical: canonical,
Data: map[string]interface{}{
@ -32,24 +32,24 @@ func serveBlogStats(w http.ResponseWriter, r *http.Request) {
var blogStatsCacheGroup singleflight.Group
func serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
data, err, _ := blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
return getBlogStats(blog)
return a.db.getBlogStats(blog)
})
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
// Render
render(w, r, templateBlogStatsTable, &renderData{
a.render(w, r, templateBlogStatsTable, &renderData{
BlogString: blog,
Data: data,
})
}
func getBlogStats(blog string) (data map[string]interface{}, err error) {
if stats := loadBlogStatsCache(blog); stats != nil {
func (db *database) getBlogStats(blog string) (data map[string]interface{}, err error) {
if stats := db.loadBlogStatsCache(blog); stats != nil {
return stats, nil
}
// Build query
@ -67,7 +67,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
Name, Posts, Chars, Words, WordsPerPost string
}
// Count total posts
row, err := appDb.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+"))", params...)
row, err := db.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+"))", params...)
if err != nil {
return nil, err
}
@ -76,7 +76,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
return nil, err
}
// Count posts per year
rows, err := appDb.query("select *, "+wordsPerPost+" from (select year, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' group by year order by year desc)", params...)
rows, err := db.query("select *, "+wordsPerPost+" from (select year, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' group by year order by year desc)", params...)
if err != nil {
return nil, err
}
@ -90,7 +90,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
}
}
// Count posts without date
row, err = appDb.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published = '')", params...)
row, err = db.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published = '')", params...)
if err != nil {
return nil, err
}
@ -102,7 +102,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
months := map[string][]statsTableType{}
month := statsTableType{}
for _, year := range years {
rows, err = appDb.query("select *, "+wordsPerPost+" from (select month, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' and year = @year group by month order by month desc)", append(params, sql.Named("year", year.Name))...)
rows, err = db.query("select *, "+wordsPerPost+" from (select month, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' and year = @year group by month order by month desc)", append(params, sql.Named("year", year.Name))...)
if err != nil {
return nil, err
}
@ -120,17 +120,17 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
"withoutdate": noDate,
"months": months,
}
cacheBlogStats(blog, data)
db.cacheBlogStats(blog, data)
return data, nil
}
func cacheBlogStats(blog string, stats map[string]interface{}) {
func (db *database) cacheBlogStats(blog string, stats map[string]interface{}) {
jb, _ := json.Marshal(stats)
_ = cachePersistently("blogstats_"+blog, jb)
_ = db.cachePersistently("blogstats_"+blog, jb)
}
func loadBlogStatsCache(blog string) (stats map[string]interface{}) {
data, err := retrievePersistentCache("blogstats_" + blog)
func (db *database) loadBlogStatsCache(blog string) (stats map[string]interface{}) {
data, err := db.retrievePersistentCache("blogstats_" + blog)
if err != nil || data == nil {
return nil
}
@ -141,6 +141,6 @@ func loadBlogStatsCache(blog string) (stats map[string]interface{}) {
return stats
}
func resetBlogStats(blog string) {
_ = clearPersistentCache("blogstats_" + blog)
func (db *database) resetBlogStats(blog string) {
_ = db.clearPersistentCache("blogstats_" + blog)
}

64
cache.go

@ -20,17 +20,22 @@ import (
"golang.org/x/sync/singleflight"
)
const (
cacheInternalExpirationHeader = "Goblog-Expire"
)
const cacheInternalExpirationHeader = "Goblog-Expire"
var (
cacheGroup singleflight.Group
cacheR *ristretto.Cache
)
type cache struct {
g singleflight.Group
c *ristretto.Cache
cfg *configCache
}
func initCache() (err error) {
cacheR, err = ristretto.NewCache(&ristretto.Config{
func (a *goBlog) initCache() (err error) {
a.cache = &cache{
cfg: a.cfg.Cache,
}
if a.cache.cfg != nil && !a.cache.cfg.Enable {
return nil
}
a.cache.c, err = ristretto.NewCache(&ristretto.Config{
NumCounters: 5000,
MaxCost: 20000000, // 20 MB
BufferItems: 16,
@ -52,13 +57,14 @@ func initCache() (err error) {
return
}
func cacheMiddleware(next http.Handler) http.Handler {
func (c *cache) cacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do checks
if !appConfig.Cache.Enable {
if c.c == nil {
// No cache configured
next.ServeHTTP(w, r)
return
}
// Do checks
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
next.ServeHTTP(w, r)
return
@ -74,32 +80,32 @@ func cacheMiddleware(next http.Handler) http.Handler {
// Search and serve cache
key := cacheKey(r)
// Get cache or render it
cacheInterface, _, _ := cacheGroup.Do(key, func() (interface{}, error) {
return getCache(key, next, r), nil
cacheInterface, _, _ := c.g.Do(key, func() (interface{}, error) {
return c.getCache(key, next, r), nil
})
cache := cacheInterface.(*cacheItem)
ci := cacheInterface.(*cacheItem)
// copy cached headers
for k, v := range cache.header {
for k, v := range ci.header {
w.Header()[k] = v
}
setCacheHeaders(w, cache)
c.setCacheHeaders(w, ci)
// check conditional request
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == cache.eTag {
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
if t, err := dateparse.ParseAny(ifModifiedSinceHeader); err == nil && t.After(cache.creationTime) {
if t, err := dateparse.ParseAny(ifModifiedSinceHeader); err == nil && t.After(ci.creationTime) {
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
}
// set status code
w.WriteHeader(cache.code)
w.WriteHeader(ci.code)
// write cached body
_, _ = w.Write(cache.body)
_, _ = w.Write(ci.body)
})
}
@ -125,14 +131,14 @@ func cacheURLString(u *url.URL) string {
return buf.String()
}
func setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
func (c *cache) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
w.Header().Set("ETag", cache.eTag)
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
if w.Header().Get("Cache-Control") == "" {
if cache.expiration != 0 {
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration))
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", appConfig.Cache.Expiration, appConfig.Cache.Expiration/3, appConfig.Cache.Expiration))
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", c.cfg.Expiration, c.cfg.Expiration/3, c.cfg.Expiration))
}
}
}
@ -146,8 +152,8 @@ type cacheItem struct {
body []byte
}
func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
if rItem, ok := cacheR.Get(key); ok {
func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
if rItem, ok := c.c.Get(key); ok {
item = rItem.(*cacheItem)
}
if item == nil {
@ -198,10 +204,10 @@ func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem)
// Save cache
if cch := item.header.Get("Cache-Control"); !strings.Contains(cch, "no-store") && !strings.Contains(cch, "private") && !strings.Contains(cch, "no-cache") {
if exp == 0 {
cacheR.Set(key, item, 0)
c.c.Set(key, item, 0)
} else {
ttl := time.Duration(exp) * time.Second
cacheR.SetWithTTL(key, item, 0, ttl)
c.c.SetWithTTL(key, item, 0, ttl)
}
}
} else {