2022-07-14 18:07:07 +01:00
|
|
|
package pageHandler
|
|
|
|
|
|
|
|
import (
|
|
|
|
"golang.captainalm.com/cityuni-webserver/conf"
|
2022-07-15 12:18:33 +01:00
|
|
|
"golang.captainalm.com/cityuni-webserver/pageHandler/utils"
|
|
|
|
"io"
|
|
|
|
"mime/multipart"
|
2022-07-14 18:07:07 +01:00
|
|
|
"net/http"
|
2022-07-15 12:18:33 +01:00
|
|
|
"net/textproto"
|
2022-07-15 10:46:45 +01:00
|
|
|
"net/url"
|
2022-07-15 12:18:33 +01:00
|
|
|
"strconv"
|
2022-07-14 23:25:43 +01:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2022-07-15 12:18:33 +01:00
|
|
|
"time"
|
2022-07-14 18:07:07 +01:00
|
|
|
)
|
|
|
|
|
2022-07-14 23:25:43 +01:00
|
|
|
type PageHandler struct {
|
2022-07-15 12:18:33 +01:00
|
|
|
PageContentsCache map[string]*CachedPage
|
2022-07-14 23:25:43 +01:00
|
|
|
PageProviders map[string]PageProvider
|
|
|
|
pageContentsCacheRWMutex *sync.RWMutex
|
|
|
|
RangeSupported bool
|
2022-07-15 13:28:29 +01:00
|
|
|
FilterURLQueries bool
|
2022-07-14 23:25:43 +01:00
|
|
|
CacheSettings conf.CacheSettingsYaml
|
|
|
|
}
|
|
|
|
|
2022-07-15 12:18:33 +01:00
|
|
|
type CachedPage struct {
|
|
|
|
Content []byte
|
|
|
|
ContentType string
|
|
|
|
LastMod time.Time
|
|
|
|
}
|
|
|
|
|
2022-07-14 23:25:43 +01:00
|
|
|
func NewPageHandler(config conf.ServeYaml) *PageHandler {
|
2022-07-15 12:18:33 +01:00
|
|
|
var thePCCMap map[string]*CachedPage
|
2022-07-14 23:25:43 +01:00
|
|
|
var theMutex *sync.RWMutex
|
|
|
|
if config.CacheSettings.EnableContentsCaching {
|
2022-07-15 12:18:33 +01:00
|
|
|
thePCCMap = make(map[string]*CachedPage)
|
2022-07-14 23:25:43 +01:00
|
|
|
theMutex = &sync.RWMutex{}
|
|
|
|
}
|
|
|
|
return &PageHandler{
|
|
|
|
PageContentsCache: thePCCMap,
|
2022-07-15 10:46:45 +01:00
|
|
|
PageProviders: GetProviders(config.CacheSettings.EnableTemplateCaching, config.DataStorage),
|
2022-07-14 23:25:43 +01:00
|
|
|
pageContentsCacheRWMutex: theMutex,
|
|
|
|
RangeSupported: config.RangeSupported,
|
2022-07-15 13:28:29 +01:00
|
|
|
FilterURLQueries: config.FilterURLQueries,
|
2022-07-14 23:25:43 +01:00
|
|
|
CacheSettings: config.CacheSettings,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
2022-07-15 12:18:33 +01:00
|
|
|
actualPagePath := strings.TrimRight(request.URL.Path, "/")
|
|
|
|
queryCollection, actualQueries := ph.GetCleanQuery(request)
|
|
|
|
|
|
|
|
var pageContent []byte
|
|
|
|
var pageContentType string
|
|
|
|
var lastMod time.Time
|
|
|
|
|
|
|
|
if ph.CacheSettings.EnableContentsCaching {
|
|
|
|
cached := ph.getPageFromCache(request.URL, actualQueries)
|
|
|
|
if cached != nil {
|
|
|
|
pageContent = cached.Content
|
|
|
|
pageContentType = cached.ContentType
|
|
|
|
lastMod = cached.LastMod
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if pageContentType == "" {
|
|
|
|
if provider := ph.PageProviders[actualPagePath]; provider != nil {
|
2022-07-15 15:37:59 +01:00
|
|
|
var canCache bool
|
|
|
|
pageContentType, pageContent, canCache = provider.GetContents(queryCollection)
|
2022-07-15 12:18:33 +01:00
|
|
|
lastMod = provider.GetLastModified()
|
2022-07-15 15:37:59 +01:00
|
|
|
if pageContentType != "" && canCache && ph.CacheSettings.EnableContentsCaching {
|
2022-07-15 12:18:33 +01:00
|
|
|
ph.setPageToCache(request.URL, actualQueries, &CachedPage{
|
|
|
|
Content: pageContent,
|
|
|
|
ContentType: pageContentType,
|
|
|
|
LastMod: lastMod,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
allowedMethods := ph.getAllowedMethodsForPath(request.URL.Path)
|
|
|
|
allowed := false
|
|
|
|
if request.Method != http.MethodOptions {
|
|
|
|
for _, method := range allowedMethods {
|
|
|
|
if method == request.Method {
|
|
|
|
allowed = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if allowed {
|
|
|
|
|
|
|
|
if pageContentType == "" {
|
|
|
|
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusNotFound, "Page Not Found")
|
|
|
|
} else {
|
|
|
|
|
|
|
|
switch request.Method {
|
|
|
|
case http.MethodGet, http.MethodHead:
|
|
|
|
|
|
|
|
writer.Header().Set("Content-Type", pageContentType)
|
|
|
|
writer.Header().Set("Content-Length", strconv.Itoa(len(pageContent)))
|
|
|
|
utils.SetLastModifiedHeader(writer.Header(), lastMod)
|
|
|
|
utils.SetCacheHeaderWithAge(writer.Header(), ph.CacheSettings.MaxAge, lastMod)
|
|
|
|
theETag := utils.GetValueForETagUsingByteArray(pageContent)
|
|
|
|
writer.Header().Set("ETag", theETag)
|
|
|
|
|
|
|
|
if utils.ProcessSupportedPreconditionsForNext(writer, request, lastMod, theETag, ph.CacheSettings.NotModifiedResponseUsingLastModified, ph.CacheSettings.NotModifiedResponseUsingETags) {
|
|
|
|
|
|
|
|
httpRangeParts := utils.ProcessRangePreconditions(int64(len(pageContent)), writer, request, lastMod, theETag, ph.RangeSupported)
|
|
|
|
if httpRangeParts != nil {
|
|
|
|
if len(httpRangeParts) <= 1 {
|
|
|
|
var theWriter io.Writer = writer
|
|
|
|
if len(httpRangeParts) == 1 {
|
|
|
|
theWriter = utils.NewPartialRangeWriter(theWriter, httpRangeParts[0])
|
|
|
|
}
|
|
|
|
_, _ = theWriter.Write(pageContent)
|
|
|
|
} else {
|
|
|
|
multWriter := multipart.NewWriter(writer)
|
|
|
|
writer.Header().Set("Content-Type", "multipart/byteranges; boundary="+multWriter.Boundary())
|
|
|
|
for _, currentPart := range httpRangeParts {
|
|
|
|
mimePart, err := multWriter.CreatePart(textproto.MIMEHeader{
|
|
|
|
"Content-Range": {currentPart.ToField(int64(len(pageContent)))},
|
|
|
|
"Content-Type": {"text/plain; charset=utf-8"},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_, err = mimePart.Write(pageContent[currentPart.Start : currentPart.Start+currentPart.Length])
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ = multWriter.Close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case http.MethodDelete:
|
|
|
|
ph.PurgeTemplateCache(actualPagePath)
|
|
|
|
ph.PurgeContentsCache(request.URL.Path, actualQueries)
|
|
|
|
utils.SetNeverCacheHeader(writer.Header())
|
|
|
|
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
|
|
|
|
theAllowHeaderContents := ""
|
|
|
|
for _, method := range allowedMethods {
|
|
|
|
theAllowHeaderContents += method + ", "
|
|
|
|
}
|
|
|
|
|
|
|
|
writer.Header().Set("Allow", strings.TrimSuffix(theAllowHeaderContents, ", "))
|
|
|
|
if request.Method == http.MethodOptions {
|
|
|
|
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
|
|
|
|
} else {
|
|
|
|
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusMethodNotAllowed, "")
|
|
|
|
}
|
|
|
|
}
|
2022-07-14 23:25:43 +01:00
|
|
|
}
|
|
|
|
|
2022-07-15 12:18:33 +01:00
|
|
|
func (ph *PageHandler) GetCleanQuery(request *http.Request) (url.Values, string) {
|
2022-07-15 10:46:45 +01:00
|
|
|
toClean := request.URL.Query()
|
|
|
|
provider := ph.PageProviders[request.URL.Path]
|
|
|
|
if provider == nil {
|
2022-07-15 12:18:33 +01:00
|
|
|
return make(url.Values), ""
|
2022-07-15 10:46:45 +01:00
|
|
|
}
|
|
|
|
supportedKeys := provider.GetSupportedURLParameters()
|
2022-07-15 13:28:29 +01:00
|
|
|
var toDelete []string
|
|
|
|
if ph.FilterURLQueries {
|
|
|
|
toDelete = make([]string, len(toClean))
|
|
|
|
}
|
2022-07-15 10:46:45 +01:00
|
|
|
theSize := 0
|
2022-07-15 12:18:33 +01:00
|
|
|
theQuery := ""
|
|
|
|
for s, v := range toClean {
|
2022-07-15 10:46:45 +01:00
|
|
|
noExist := true
|
|
|
|
for _, key := range supportedKeys {
|
|
|
|
if s == key {
|
|
|
|
noExist = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if noExist {
|
2022-07-15 13:28:29 +01:00
|
|
|
if ph.FilterURLQueries {
|
|
|
|
toDelete[theSize] = s
|
|
|
|
theSize++
|
|
|
|
}
|
2022-07-15 12:18:33 +01:00
|
|
|
} else {
|
|
|
|
for _, i := range v {
|
|
|
|
if i == "" {
|
|
|
|
theQuery += s + "&"
|
|
|
|
} else {
|
|
|
|
theQuery += s + "=" + i + "&"
|
|
|
|
}
|
|
|
|
}
|
2022-07-15 10:46:45 +01:00
|
|
|
}
|
|
|
|
}
|
2022-07-15 13:28:29 +01:00
|
|
|
if ph.FilterURLQueries {
|
|
|
|
for i := 0; i < theSize; i++ {
|
|
|
|
delete(toClean, toDelete[i])
|
|
|
|
}
|
2022-07-15 10:46:45 +01:00
|
|
|
}
|
2022-07-15 12:18:33 +01:00
|
|
|
return toClean, strings.TrimRight(theQuery, "&")
|
2022-07-15 10:46:45 +01:00
|
|
|
}
|
|
|
|
|
2022-07-14 23:25:43 +01:00
|
|
|
func (ph *PageHandler) PurgeContentsCache(path string, query string) {
|
2022-07-15 12:18:33 +01:00
|
|
|
if ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge {
|
2022-07-14 23:25:43 +01:00
|
|
|
if path == "" {
|
|
|
|
ph.pageContentsCacheRWMutex.Lock()
|
2022-07-15 12:18:33 +01:00
|
|
|
ph.PageContentsCache = make(map[string]*CachedPage)
|
2022-07-14 23:25:43 +01:00
|
|
|
ph.pageContentsCacheRWMutex.Unlock()
|
|
|
|
} else {
|
|
|
|
if strings.HasSuffix(path, "/") {
|
|
|
|
ph.pageContentsCacheRWMutex.RLock()
|
|
|
|
toDelete := make([]string, len(ph.PageContentsCache))
|
|
|
|
theSize := 0
|
|
|
|
for cPath := range ph.PageContentsCache {
|
|
|
|
dPath := strings.Split(cPath, "?")[0]
|
|
|
|
if dPath == path || dPath == path[:len(path)-1] {
|
|
|
|
toDelete[theSize] = cPath
|
|
|
|
theSize++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ph.pageContentsCacheRWMutex.RUnlock()
|
|
|
|
ph.pageContentsCacheRWMutex.Lock()
|
|
|
|
for i := 0; i < theSize; i++ {
|
|
|
|
delete(ph.PageContentsCache, toDelete[i])
|
|
|
|
}
|
|
|
|
ph.pageContentsCacheRWMutex.Unlock()
|
|
|
|
} else {
|
|
|
|
ph.pageContentsCacheRWMutex.Lock()
|
|
|
|
if query == "" {
|
|
|
|
delete(ph.PageContentsCache, path)
|
|
|
|
} else {
|
|
|
|
delete(ph.PageContentsCache, path+"?"+query)
|
|
|
|
}
|
|
|
|
ph.pageContentsCacheRWMutex.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-14 18:07:07 +01:00
|
|
|
|
2022-07-14 23:25:43 +01:00
|
|
|
func (ph *PageHandler) PurgeTemplateCache(path string) {
|
|
|
|
if ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge {
|
|
|
|
if path == "" {
|
|
|
|
for _, pageProvider := range ph.PageProviders {
|
|
|
|
pageProvider.PurgeTemplate()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if pageProvider, ok := ph.PageProviders[path]; ok {
|
|
|
|
pageProvider.PurgeTemplate()
|
|
|
|
}
|
|
|
|
}
|
2022-07-14 18:07:07 +01:00
|
|
|
}
|
|
|
|
}
|
2022-07-15 12:18:33 +01:00
|
|
|
func (ph *PageHandler) getPageFromCache(urlIn *url.URL, cleanedQueries string) *CachedPage {
|
|
|
|
ph.pageContentsCacheRWMutex.RLock()
|
|
|
|
defer ph.pageContentsCacheRWMutex.RUnlock()
|
|
|
|
if strings.HasSuffix(urlIn.Path, "/") {
|
|
|
|
return ph.PageContentsCache[strings.TrimRight(urlIn.Path, "/")]
|
|
|
|
} else {
|
|
|
|
if cleanedQueries == "" {
|
|
|
|
return ph.PageContentsCache[urlIn.Path]
|
|
|
|
} else {
|
|
|
|
return ph.PageContentsCache[urlIn.Path+"?"+cleanedQueries]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ph *PageHandler) setPageToCache(urlIn *url.URL, cleanedQueries string, newPage *CachedPage) {
|
|
|
|
ph.pageContentsCacheRWMutex.Lock()
|
|
|
|
defer ph.pageContentsCacheRWMutex.Unlock()
|
|
|
|
if strings.HasSuffix(urlIn.Path, "/") {
|
|
|
|
ph.PageContentsCache[strings.TrimRight(urlIn.Path, "/")] = newPage
|
|
|
|
} else {
|
|
|
|
if cleanedQueries == "" {
|
|
|
|
ph.PageContentsCache[urlIn.Path] = newPage
|
|
|
|
} else {
|
|
|
|
ph.PageContentsCache[urlIn.Path+"?"+cleanedQueries] = newPage
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ph *PageHandler) getAllowedMethodsForPath(pathIn string) []string {
|
|
|
|
if strings.HasSuffix(pathIn, "/") {
|
|
|
|
if (ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge) ||
|
|
|
|
(ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge) {
|
|
|
|
return []string{http.MethodHead, http.MethodGet, http.MethodOptions, http.MethodDelete}
|
|
|
|
} else {
|
|
|
|
return []string{http.MethodHead, http.MethodGet, http.MethodOptions}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge {
|
|
|
|
return []string{http.MethodHead, http.MethodGet, http.MethodOptions, http.MethodDelete}
|
|
|
|
} else {
|
|
|
|
return []string{http.MethodHead, http.MethodGet, http.MethodOptions}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|