package webserver import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path" fp "path/filepath" "strconv" "strings" "github.com/PuerkitoBio/goquery" "github.com/go-shiori/shiori/internal/model" "github.com/go-shiori/warc" "github.com/julienschmidt/httprouter" ) // serveFile is handler for general file request func (h *handler) serveFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { rootPath := strings.Trim(h.RootPath, "/") urlPath := strings.Trim(r.URL.Path, "/") filePath := strings.TrimPrefix(urlPath, rootPath) err := serveFile(w, filePath, true) checkError(err) } // serveJsFile is handler for GET /js/*filepath func (h *handler) serveJsFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { jsFilePath := ps.ByName("filepath") jsFilePath = path.Join("js", jsFilePath) jsDir, jsName := path.Split(jsFilePath) if developmentMode && fp.Ext(jsName) == ".js" && strings.HasSuffix(jsName, ".min.js") { jsName = strings.TrimSuffix(jsName, ".min.js") + ".js" tmpPath := path.Join(jsDir, jsName) if assetExists(tmpPath) { jsFilePath = tmpPath } } err := serveFile(w, jsFilePath, true) checkError(err) } // serveIndexPage is handler for GET / func (h *handler) serveIndexPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Make sure session still valid err := h.validateSession(r) if err != nil { newPath := path.Join(h.RootPath, "/login") redirectURL := createRedirectURL(newPath, r.URL.String()) redirectPage(w, r, redirectURL) return } if developmentMode { h.prepareTemplates() } err = h.templates["index"].Execute(w, h.RootPath) checkError(err) } // serveLoginPage is handler for GET /login func (h *handler) serveLoginPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Make sure session is not valid err := h.validateSession(r) if err == nil { redirectURL := path.Join(h.RootPath, "/") redirectPage(w, r, redirectURL) return } if developmentMode { h.prepareTemplates() } err = h.templates["login"].Execute(w, h.RootPath) checkError(err) } // serveBookmarkContent is handler for GET /bookmark/:id/content func (h *handler) serveBookmarkContent(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Get bookmark ID from URL strID := ps.ByName("id") id, err := strconv.Atoi(strID) checkError(err) // Get bookmark in database bookmark, exist := h.DB.GetBookmark(id, "") if !exist { panic(fmt.Errorf("Bookmark not found")) } // If it's not public, make sure session still valid if bookmark.Public != 1 { err = h.validateSession(r) if err != nil { newPath := path.Join(h.RootPath, "/login") redirectURL := createRedirectURL(newPath, r.URL.String()) redirectPage(w, r, redirectURL) return } } // Check if it has archive. archivePath := fp.Join(h.DataDir, "archive", strID) if fileExists(archivePath) { bookmark.HasArchive = true // Open archive, look in cache first var archive *warc.Archive cacheData, found := h.ArchiveCache.Get(strID) if found { archive = cacheData.(*warc.Archive) } else { archivePath := fp.Join(h.DataDir, "archive", strID) archive, err = warc.Open(archivePath) checkError(err) h.ArchiveCache.Set(strID, archive, 0) } // Find all image and convert its source to use the archive URL. createArchivalURL := func(archivalName string) string { archivalURL := *r.URL archivalURL.Path = path.Join(h.RootPath, "bookmark", strID, "archive", archivalName) return archivalURL.String() } buffer := strings.NewReader(bookmark.HTML) doc, err := goquery.NewDocumentFromReader(buffer) checkError(err) doc.Find("img, picture, figure, source").Each(func(_ int, node *goquery.Selection) { // Get the needed attributes src, _ := node.Attr("src") strSrcSets, _ := node.Attr("srcset") // Convert `src` attributes if src != "" { archivalName := getArchivalName(src) if archivalName != "" && archive.HasResource(archivalName) { node.SetAttr("src", createArchivalURL(archivalName)) } } // Split srcset by comma, then process it like any URLs srcSets := strings.Split(strSrcSets, ",") for i, srcSet := range srcSets { srcSet = strings.TrimSpace(srcSet) parts := strings.SplitN(srcSet, " ", 2) if parts[0] == "" { continue } archivalName := getArchivalName(parts[0]) if archivalName != "" && archive.HasResource(archivalName) { archivalURL := createArchivalURL(archivalName) srcSets[i] = strings.Replace(srcSets[i], parts[0], archivalURL, 1) } } if len(srcSets) > 0 { node.SetAttr("srcset", strings.Join(srcSets, ",")) } }) bookmark.HTML, err = goquery.OuterHtml(doc.Selection) checkError(err) } // Execute template if developmentMode { h.prepareTemplates() } tplData := struct { RootPath string Book model.Bookmark }{h.RootPath, bookmark} err = h.templates["content"].Execute(w, &tplData) checkError(err) } // serveThumbnailImage is handler for GET /bookmark/:id/thumb func (h *handler) serveThumbnailImage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Get bookmark ID from URL id := ps.ByName("id") // Open image imgPath := fp.Join(h.DataDir, "thumb", id) img, err := os.Open(imgPath) checkError(err) defer img.Close() // Get image type from its 512 first bytes buffer := make([]byte, 512) _, err = img.Read(buffer) checkError(err) mimeType := http.DetectContentType(buffer) w.Header().Set("Content-Type", mimeType) // Set cache value info, err := img.Stat() checkError(err) etag := fmt.Sprintf(`W/"%x-%x"`, info.ModTime().Unix(), info.Size()) w.Header().Set("ETag", etag) w.Header().Set("Cache-Control", "max-age=86400") // Serve image img.Seek(0, 0) _, err = io.Copy(w, img) checkError(err) } // serveBookmarkArchive is handler for GET /bookmark/:id/archive/*filepath func (h *handler) serveBookmarkArchive(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Get parameter from URL strID := ps.ByName("id") resourcePath := ps.ByName("filepath") resourcePath = strings.TrimPrefix(resourcePath, "/") // Get bookmark from database id, err := strconv.Atoi(strID) checkError(err) bookmark, exist := h.DB.GetBookmark(id, "") if !exist { panic(fmt.Errorf("Bookmark not found")) } // If it's not public, make sure session still valid if bookmark.Public != 1 { err = h.validateSession(r) if err != nil { newPath := path.Join(h.RootPath, "/login") redirectURL := createRedirectURL(newPath, r.URL.String()) redirectPage(w, r, redirectURL) return } } // Open archive, look in cache first var archive *warc.Archive cacheData, found := h.ArchiveCache.Get(strID) if found { archive = cacheData.(*warc.Archive) } else { archivePath := fp.Join(h.DataDir, "archive", strID) archive, err = warc.Open(archivePath) checkError(err) h.ArchiveCache.Set(strID, archive, 0) } content, contentType, err := archive.Read(resourcePath) checkError(err) // Set response header w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Type", contentType) // If this is HTML and root, inject shiori header if strings.Contains(strings.ToLower(contentType), "text/html") && resourcePath == "" { // Extract gzip buffer := bytes.NewBuffer(content) gzipReader, err := gzip.NewReader(buffer) checkError(err) // Parse gzipped content doc, err := goquery.NewDocumentFromReader(gzipReader) checkError(err) // Add Shiori overlay tplOutput := bytes.NewBuffer(nil) err = h.templates["archive"].Execute(tplOutput, &bookmark) checkError(err) archiveCSSPath := path.Join(h.RootPath, "/css/archive.css") sourceSansProCSSPath := path.Join(h.RootPath, "/css/source-sans-pro.min.css") docHead := doc.Find("head") docHead.PrependHtml(``) docHead.AppendHtml(``) docHead.AppendHtml(``) doc.Find("body").PrependHtml(tplOutput.String()) // Revert back to HTML outerHTML, err := goquery.OuterHtml(doc.Selection) checkError(err) // Gzip it again and send to response writer gzipWriter := gzip.NewWriter(w) gzipWriter.Write([]byte(outerHTML)) gzipWriter.Flush() return } // Serve content w.Write(content) }