github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/extend/plugin_hyperdrive.go (about)

     1  // Highly experimental plugin for caching rendered pages for guests
     2  package extend
     3  
     4  import (
     5  	//"log"
     6  	"bytes"
     7  	"errors"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"strconv"
    11  	"strings"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	c "github.com/Azareal/Gosora/common"
    16  	"github.com/Azareal/Gosora/routes"
    17  )
    18  
    19  var hyperspace *Hyperspace
    20  
    21  func init() {
    22  	c.Plugins.Add(&c.Plugin{UName: "hyperdrive", Name: "Hyperdrive", Author: "Azareal", Init: initHdrive, Deactivate: deactivateHdrive})
    23  }
    24  
    25  func initHdrive(pl *c.Plugin) error {
    26  	hyperspace = newHyperspace()
    27  	pl.AddHook("tasks_tick_topic_list", tickHdrive)
    28  	pl.AddHook("tasks_tick_widget_wol", tickHdriveWol)
    29  	pl.AddHook("route_topic_list_start", jumpHdriveTopicList)
    30  	pl.AddHook("route_forum_list_start", jumpHdriveForumList)
    31  	tickHdrive()
    32  	return nil
    33  }
    34  
    35  func deactivateHdrive(pl *c.Plugin) {
    36  	pl.RemoveHook("tasks_tick_topic_list", tickHdrive)
    37  	pl.RemoveHook("tasks_tick_widget_wol", tickHdriveWol)
    38  	pl.RemoveHook("route_topic_list_start", jumpHdriveTopicList)
    39  	pl.RemoveHook("route_forum_list_start", jumpHdriveForumList)
    40  	hyperspace = nil
    41  }
    42  
    43  type Hyperspace struct {
    44  	topicList           atomic.Value
    45  	forumList           atomic.Value
    46  	lastTopicListUpdate atomic.Value
    47  }
    48  
    49  func newHyperspace() *Hyperspace {
    50  	pageCache := new(Hyperspace)
    51  	blank := make(map[string][3][]byte, len(c.Themes))
    52  	pageCache.topicList.Store(blank)
    53  	pageCache.forumList.Store(blank)
    54  	pageCache.lastTopicListUpdate.Store(int64(0))
    55  	return pageCache
    56  }
    57  
    58  func tickHdriveWol(args ...interface{}) (skip bool, rerr c.RouteError) {
    59  	c.DebugLog("docking at wol")
    60  	return tickHdrive(args)
    61  }
    62  
    63  // TODO: Find a better way of doing this
    64  func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
    65  	c.DebugLog("Refueling...")
    66  
    67  	// Avoid accidentally caching already cached content
    68  	blank := make(map[string][3][]byte, len(c.Themes))
    69  	hyperspace.topicList.Store(blank)
    70  	hyperspace.forumList.Store(blank)
    71  
    72  	tListMap := make(map[string][3][]byte)
    73  	fListMap := make(map[string][3][]byte)
    74  
    75  	cacheTheme := func(tname string) (skip, fail bool, rerr c.RouteError) {
    76  
    77  		themeCookie := http.Cookie{Name: "current_theme", Value: tname, Path: "/", MaxAge: c.Year}
    78  
    79  		w := httptest.NewRecorder()
    80  		req := httptest.NewRequest("get", "/topics/", bytes.NewReader(nil))
    81  		req.AddCookie(&themeCookie)
    82  		user := c.GuestUser
    83  
    84  		head, rerr := c.UserCheck(w, req, &user)
    85  		if rerr != nil {
    86  			return true, true, rerr
    87  		}
    88  		rerr = routes.TopicList(w, req, &user, head)
    89  		if rerr != nil {
    90  			return true, true, rerr
    91  		}
    92  		if w.Code != 200 {
    93  			c.LogWarning(errors.New("not 200 for topic list in hyperdrive"))
    94  			return false, true, nil
    95  		}
    96  
    97  		buf := new(bytes.Buffer)
    98  		buf.ReadFrom(w.Result().Body)
    99  
   100  		gbuf, err := c.CompressBytesGzip(buf.Bytes())
   101  		if err != nil {
   102  			c.LogWarning(err)
   103  			return false, true, nil
   104  		}
   105  
   106  		bbuf, err := c.CompressBytesBrotli(buf.Bytes())
   107  		if err != nil {
   108  			c.LogWarning(err)
   109  			return false, true, nil
   110  		}
   111  		tListMap[tname] = [3][]byte{buf.Bytes(), gbuf, bbuf}
   112  
   113  		w = httptest.NewRecorder()
   114  		req = httptest.NewRequest("get", "/forums/", bytes.NewReader(nil))
   115  		user = c.GuestUser
   116  
   117  		head, rerr = c.UserCheck(w, req, &user)
   118  		if rerr != nil {
   119  			return true, true, rerr
   120  		}
   121  		rerr = routes.ForumList(w, req, &user, head)
   122  		if rerr != nil {
   123  			return true, true, rerr
   124  		}
   125  		if w.Code != 200 {
   126  			c.LogWarning(errors.New("not 200 for forum list in hyperdrive"))
   127  			return false, true, nil
   128  		}
   129  
   130  		buf = new(bytes.Buffer)
   131  		buf.ReadFrom(w.Result().Body)
   132  
   133  		gbuf, err = c.CompressBytesGzip(buf.Bytes())
   134  		if err != nil {
   135  			c.LogWarning(err)
   136  			return false, true, nil
   137  		}
   138  
   139  		bbuf, err = c.CompressBytesBrotli(buf.Bytes())
   140  		if err != nil {
   141  			c.LogWarning(err)
   142  			return false, true, nil
   143  		}
   144  		fListMap[tname] = [3][]byte{buf.Bytes(), gbuf, bbuf}
   145  		return false, false, nil
   146  	}
   147  
   148  	for tname, _ := range c.Themes {
   149  		skip, fail, rerr := cacheTheme(tname)
   150  		if fail || rerr != nil {
   151  			return skip, rerr
   152  		}
   153  	}
   154  
   155  	hyperspace.topicList.Store(tListMap)
   156  	hyperspace.forumList.Store(fListMap)
   157  	hyperspace.lastTopicListUpdate.Store(time.Now().Unix())
   158  
   159  	return false, nil
   160  }
   161  
   162  func jumpHdriveTopicList(args ...interface{}) (skip bool, rerr c.RouteError) {
   163  	theme := c.GetThemeByReq(args[1].(*http.Request))
   164  	p := hyperspace.topicList.Load().(map[string][3][]byte)
   165  	return jumpHdrive(p[theme.Name], args)
   166  }
   167  
   168  func jumpHdriveForumList(args ...interface{}) (skip bool, rerr c.RouteError) {
   169  	theme := c.GetThemeByReq(args[1].(*http.Request))
   170  	p := hyperspace.forumList.Load().(map[string][3][]byte)
   171  	return jumpHdrive(p[theme.Name], args)
   172  }
   173  
   174  func jumpHdrive( /*pg, */ p [3][]byte, args []interface{}) (skip bool, rerr c.RouteError) {
   175  	var tList []byte
   176  	w := args[0].(http.ResponseWriter)
   177  	r := args[1].(*http.Request)
   178  	var iw http.ResponseWriter
   179  	gzw, ok := w.(c.GzipResponseWriter)
   180  	//bzw, ok2 := w.(c.BrResponseWriter)
   181  	// !temp until global brotli
   182  	br := strings.Contains(r.Header.Get("Accept-Encoding"), "br")
   183  	if br && ok {
   184  		tList = p[2]
   185  		iw = gzw.ResponseWriter
   186  	} else if br {
   187  		tList = p[2]
   188  		iw = w
   189  	} else if ok {
   190  		tList = p[1]
   191  		iw = gzw.ResponseWriter
   192  		/*} else if ok2 {
   193  		tList = p[2]
   194  		iw = bzw.ResponseWriter
   195  		*/
   196  	} else {
   197  		tList = p[0]
   198  		iw = w
   199  	}
   200  	if len(tList) == 0 {
   201  		c.DebugLog("no itemlist in hyperspace")
   202  		return false, nil
   203  	}
   204  	//c.DebugLog("tList: ", tList)
   205  
   206  	// Avoid intercepting user requests as we only have guests in cache right now
   207  	user := args[2].(*c.User)
   208  	if user.ID != 0 {
   209  		c.DebugLog("not guest")
   210  		return false, nil
   211  	}
   212  
   213  	// Avoid intercepting search requests and filters as we don't have those in cache
   214  	//c.DebugLog("r.URL.Path:",r.URL.Path)
   215  	//c.DebugLog("r.URL.RawQuery:",r.URL.RawQuery)
   216  	if r.URL.RawQuery != "" {
   217  		return false, nil
   218  	}
   219  	if r.FormValue("js") == "1" || r.FormValue("i") == "1" {
   220  		return false, nil
   221  	}
   222  	c.DebugLog("Successful jump")
   223  
   224  	var etag string
   225  	lastUpdate := hyperspace.lastTopicListUpdate.Load().(int64)
   226  	c.DebugLog("lastUpdate:", lastUpdate)
   227  	if br {
   228  		h := iw.Header()
   229  		h.Set("X-I", "1")
   230  		h.Set("Content-Encoding", "br")
   231  		etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "-b\""
   232  	} else if ok {
   233  		iw.Header().Set("X-I", "1")
   234  		etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "-g\""
   235  		/*} else if ok2 {
   236  		iw.Header().Set("X-I", "1")
   237  		etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "-b\""
   238  		*/
   239  	} else {
   240  		etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "\""
   241  	}
   242  
   243  	if lastUpdate != 0 {
   244  		iw.Header().Set("ETag", etag)
   245  		if match := r.Header.Get("If-None-Match"); match != "" {
   246  			if strings.Contains(match, etag) {
   247  				iw.WriteHeader(http.StatusNotModified)
   248  				return true, nil
   249  			}
   250  		}
   251  	}
   252  
   253  	header := args[3].(*c.Header)
   254  	if br || ok /*ok2*/ {
   255  		iw.Header().Set("Content-Type", "text/html;charset=utf-8")
   256  	}
   257  	routes.FootHeaders(w, header)
   258  	iw.Write(tList)
   259  
   260  	return true, nil
   261  }