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 }