wrp/wrp.go

692 lines
20 KiB
Go
Raw Normal View History

2019-05-30 01:53:05 +00:00
//
// WRP - Web Rendering Proxy
//
2024-05-23 07:34:00 +00:00
// Copyright (c) 2013-2024 Antoni Sawicki
2024-01-02 10:09:38 +00:00
// Copyright (c) 2019-2024 Google LLC
2019-05-30 01:53:05 +00:00
//
2019-05-29 08:29:01 +00:00
package main
import (
"bytes"
2019-05-29 08:52:28 +00:00
"context"
2022-03-17 02:27:34 +00:00
"embed"
2019-05-29 08:52:28 +00:00
"flag"
"fmt"
"image"
"image/color/palette"
"image/gif"
2022-12-08 08:55:56 +00:00
"image/jpeg"
"image/png"
"io"
2020-10-29 14:16:14 +00:00
"io/ioutil"
2019-05-29 08:52:28 +00:00
"log"
"math"
2019-05-30 09:03:17 +00:00
"math/rand"
2022-12-08 09:49:46 +00:00
"net"
2019-05-29 08:52:28 +00:00
"net/http"
"net/url"
2019-07-17 05:29:35 +00:00
"os"
"os/signal"
2024-05-23 07:34:00 +00:00
"runtime"
2019-05-29 08:52:28 +00:00
"strconv"
"strings"
2019-07-17 05:29:35 +00:00
"syscall"
2024-06-20 06:37:44 +00:00
"text/template"
2019-05-29 08:52:28 +00:00
"time"
2019-05-29 08:29:01 +00:00
h2m "github.com/JohannesKaufmann/html-to-markdown"
2024-06-22 19:14:39 +00:00
"github.com/JohannesKaufmann/html-to-markdown/plugin"
2020-09-28 19:19:27 +00:00
"github.com/MaxHalford/halfgone"
2019-08-13 06:35:29 +00:00
"github.com/chromedp/cdproto/css"
2019-05-29 09:39:06 +00:00
"github.com/chromedp/cdproto/emulation"
2024-06-19 07:35:12 +00:00
"github.com/chromedp/cdproto/input"
"github.com/chromedp/cdproto/page"
2019-05-29 08:52:28 +00:00
"github.com/chromedp/chromedp"
2022-11-10 04:48:18 +00:00
"github.com/soniakeys/quant/median"
2024-06-22 22:17:26 +00:00
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
2019-05-29 08:29:01 +00:00
)
2024-05-23 07:34:46 +00:00
const version = "4.6.3"
2022-12-08 07:44:09 +00:00
2019-05-29 08:29:01 +00:00
var (
addr = flag.String("l", ":8080", "Listen address:port, default :8080")
headless = flag.Bool("h", true, "Headless mode / hide browser window (default true)")
noDel = flag.Bool("n", false, "Do not free maps and images after use")
2022-12-08 08:55:56 +00:00
defType = flag.String("t", "gif", "Image type: png|gif|jpg")
jpgQual = flag.Int("q", 80, "Jpeg image quality, default 80%")
fgeom = flag.String("g", "1152x600x216", "Geometry: width x height x colors, height can be 0 for unlimited")
htmFnam = flag.String("ui", "wrp.html", "HTML template file for the UI")
delay = flag.Duration("s", 2*time.Second, "Delay/sleep after page is rendered and before screenshot is taken")
2024-01-03 05:54:59 +00:00
userAgent = flag.String("ua", "", "override chrome user agent")
srv http.Server
actx, ctx context.Context
acncl, cncl context.CancelFunc
img = make(map[string]bytes.Buffer)
ismap = make(map[string]wrpReq)
defGeom geom
htmlTmpl *template.Template
2019-05-29 08:29:01 +00:00
)
2022-11-30 10:00:53 +00:00
//go:embed *.html
2022-03-17 02:27:34 +00:00
var fs embed.FS
type geom struct {
w int64
h int64
c int64
}
// Data for html template
type uiData struct {
Version string
URL string
2020-10-31 15:51:20 +00:00
BgColor string
NColors int64
Width int64
Height int64
2021-03-08 13:02:22 +00:00
Zoom float64
ImgType string
2020-10-31 15:51:20 +00:00
ImgURL string
ImgSize string
ImgWidth int
ImgHeight int
2020-10-31 15:51:20 +00:00
MapURL string
PageHeight string
2024-06-20 06:37:44 +00:00
TeXT string
}
// Parameters for HTML print function
type printParams struct {
2020-10-31 15:51:20 +00:00
bgColor string
pageHeight string
imgSize string
imgURL string
mapURL string
imgWidth int
imgHeight int
2024-06-20 06:37:44 +00:00
text string
}
2020-04-24 10:06:21 +00:00
// WRP Request
2019-07-11 06:58:40 +00:00
type wrpReq struct {
url string // url
width int64 // width
height int64 // height
2021-03-08 13:02:22 +00:00
zoom float64 // zoom/scale
colors int64 // #colors
mouseX int64 // mouseX
mouseY int64 // mouseY
keys string // keys to send
buttons string // Fn buttons
imgType string // imgtype
2022-11-06 09:11:49 +00:00
w http.ResponseWriter
r *http.Request
2019-06-26 00:07:43 +00:00
}
2019-06-24 07:40:34 +00:00
2020-04-24 10:06:21 +00:00
// Parse HTML Form, Process Input Boxes, Etc.
func (rq *wrpReq) parseForm() {
2022-11-06 09:11:49 +00:00
rq.r.ParseForm()
rq.url = rq.r.FormValue("url")
if len(rq.url) > 1 && !strings.HasPrefix(rq.url, "http") {
rq.url = fmt.Sprintf("http://www.google.com/search?q=%s", url.QueryEscape(rq.url))
2019-06-26 00:07:43 +00:00
}
2022-11-06 09:11:49 +00:00
rq.width, _ = strconv.ParseInt(rq.r.FormValue("w"), 10, 64)
rq.height, _ = strconv.ParseInt(rq.r.FormValue("h"), 10, 64)
if rq.width < 10 && rq.height < 10 {
rq.width = defGeom.w
rq.height = defGeom.h
2019-05-29 08:52:28 +00:00
}
2022-11-06 09:11:49 +00:00
rq.zoom, _ = strconv.ParseFloat(rq.r.FormValue("z"), 64)
if rq.zoom < 0.1 {
rq.zoom = 1.0
2019-05-31 01:08:48 +00:00
}
2022-11-06 09:11:49 +00:00
rq.colors, _ = strconv.ParseInt(rq.r.FormValue("c"), 10, 64)
if rq.colors < 2 || rq.colors > 256 {
rq.colors = defGeom.c
2019-06-03 00:06:41 +00:00
}
2022-11-06 09:11:49 +00:00
rq.keys = rq.r.FormValue("k")
rq.buttons = rq.r.FormValue("Fn")
rq.imgType = rq.r.FormValue("t")
2022-12-08 08:55:56 +00:00
switch rq.imgType {
case "png":
case "gif":
case "jpg":
2024-06-20 06:37:44 +00:00
case "txt":
2022-12-08 08:55:56 +00:00
default:
2022-12-08 07:44:09 +00:00
rq.imgType = *defType
2019-11-04 00:58:38 +00:00
}
2022-11-06 09:11:49 +00:00
log.Printf("%s WrpReq from UI Form: %+v\n", rq.r.RemoteAddr, rq)
2019-06-26 00:07:43 +00:00
}
2020-04-24 10:06:21 +00:00
// Display WP UI
func (rq *wrpReq) printHTML(p printParams) {
2022-11-06 09:11:49 +00:00
rq.w.Header().Set("Cache-Control", "max-age=0")
rq.w.Header().Set("Expires", "-1")
rq.w.Header().Set("Pragma", "no-cache")
rq.w.Header().Set("Content-Type", "text/html")
data := uiData{
Version: version,
URL: rq.url,
2020-10-31 15:51:20 +00:00
BgColor: p.bgColor,
Width: rq.width,
Height: rq.height,
NColors: rq.colors,
Zoom: rq.zoom,
ImgType: rq.imgType,
2020-10-31 15:51:20 +00:00
ImgSize: p.imgSize,
ImgWidth: p.imgWidth,
ImgHeight: p.imgHeight,
ImgURL: p.imgURL,
MapURL: p.mapURL,
PageHeight: p.pageHeight,
2024-06-20 06:37:44 +00:00
TeXT: p.text,
2019-11-04 01:53:20 +00:00
}
2022-11-06 09:11:49 +00:00
err := htmlTmpl.Execute(rq.w, data)
if err != nil {
log.Fatal(err)
2019-11-04 00:58:38 +00:00
}
2019-06-26 08:07:13 +00:00
}
2020-10-26 08:21:42 +00:00
// Determine what action to take
func (rq *wrpReq) action() chromedp.Action {
2020-04-24 10:09:20 +00:00
// Mouse Click
if rq.mouseX > 0 && rq.mouseY > 0 {
2022-11-06 09:11:49 +00:00
log.Printf("%s Mouse Click %d,%d\n", rq.r.RemoteAddr, rq.mouseX, rq.mouseY)
return chromedp.MouseClickXY(float64(rq.mouseX)/float64(rq.zoom), float64(rq.mouseY)/float64(rq.zoom))
2020-10-26 08:21:42 +00:00
}
// Buttons
if len(rq.buttons) > 0 {
2022-11-06 09:11:49 +00:00
log.Printf("%s Button %v\n", rq.r.RemoteAddr, rq.buttons)
switch rq.buttons {
case "Bk":
2020-10-26 08:21:42 +00:00
return chromedp.NavigateBack()
case "St":
return chromedp.Stop()
case "Re":
return chromedp.Reload()
case "Bs":
2020-10-26 08:21:42 +00:00
return chromedp.KeyEvent("\b")
case "Rt":
2020-10-26 08:21:42 +00:00
return chromedp.KeyEvent("\r")
case "<":
2020-10-26 08:21:42 +00:00
return chromedp.KeyEvent("\u0302")
case "^":
2020-10-26 08:21:42 +00:00
return chromedp.KeyEvent("\u0304")
case "v":
2020-10-26 08:21:42 +00:00
return chromedp.KeyEvent("\u0301")
case ">":
2020-10-26 08:21:42 +00:00
return chromedp.KeyEvent("\u0303")
2024-06-19 07:35:12 +00:00
case "Up":
return chromedp.KeyEvent("\u0308")
case "Dn":
return chromedp.KeyEvent("\u0307")
case "All": // Select all
return chromedp.KeyEvent("a", chromedp.KeyModifiers(input.ModifierCtrl))
}
2020-10-26 08:21:42 +00:00
}
// Keys
if len(rq.keys) > 0 {
2022-11-06 09:11:49 +00:00
log.Printf("%s Sending Keys: %#v\n", rq.r.RemoteAddr, rq.keys)
return chromedp.KeyEvent(rq.keys)
2019-07-10 08:01:40 +00:00
}
2020-10-26 08:21:42 +00:00
// Navigate to URL
2024-06-20 06:37:44 +00:00
log.Printf("%s Processing Navigate Request for %s\n", rq.r.RemoteAddr, rq.url)
return chromedp.Navigate(rq.url)
2020-10-26 08:21:42 +00:00
}
// Navigate to the desired URL.
func (rq *wrpReq) navigate() {
ctxErr(chromedp.Run(ctx, rq.action()), rq.w)
}
// Handle context errors
func ctxErr(err error, w io.Writer) {
2022-12-08 08:28:28 +00:00
// TODO: callers should have retry logic, perhaps create another function
// that takes ...chromedp.Action and retries with give up
if err == nil {
return
}
log.Printf("Context error: %s", err)
fmt.Fprintf(w, "Context error: %s<BR>\n", err)
if err.Error() != "context canceled" {
return
2019-05-31 07:41:46 +00:00
}
ctx, cncl = chromedp.NewContext(actx)
log.Printf("Created new context, try again")
fmt.Fprintln(w, "Created new context, try again")
2020-04-24 09:45:34 +00:00
}
2022-11-06 08:47:59 +00:00
// https://github.com/chromedp/chromedp/issues/979
func chromedpCaptureScreenshot(res *[]byte, h int64) chromedp.Action {
if res == nil {
panic("res cannot be nil")
}
if h == 0 {
return chromedp.CaptureScreenshot(res)
}
return chromedp.ActionFunc(func(ctx context.Context) error {
var err error
*res, err = page.CaptureScreenshot().Do(ctx)
return err
})
}
2022-11-29 13:50:17 +00:00
func gifPalette(i image.Image, n int64) image.Image {
switch n {
case 2:
i = halfgone.FloydSteinbergDitherer{}.Apply(halfgone.ImageToGray(i))
case 216:
var FastGifLut = [256]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}
r := i.Bounds()
// NOTE: the color index computation below works only for palette.WebSafe!
p := image.NewPaletted(r, palette.WebSafe)
if i64, ok := i.(image.RGBA64Image); ok {
for y := r.Min.Y; y < r.Max.Y; y++ {
for x := r.Min.X; x < r.Max.X; x++ {
c := i64.RGBA64At(x, y)
r6 := FastGifLut[c.R>>8]
g6 := FastGifLut[c.G>>8]
b6 := FastGifLut[c.B>>8]
p.SetColorIndex(x, y, uint8(36*r6+6*g6+b6))
}
}
} else {
for y := r.Min.Y; y < r.Max.Y; y++ {
for x := r.Min.X; x < r.Max.X; x++ {
c := i.At(x, y)
r, g, b, _ := c.RGBA()
r6 := FastGifLut[r&0xff]
g6 := FastGifLut[g&0xff]
b6 := FastGifLut[b&0xff]
p.SetColorIndex(x, y, uint8(36*r6+6*g6+b6))
}
}
}
i = p
default:
q := median.Quantizer(n)
i = q.Paletted(i)
}
return i
}
2020-04-24 10:06:21 +00:00
// Capture currently rendered web page to an image and fake ISMAP
func (rq *wrpReq) capture() {
2020-04-23 10:25:39 +00:00
var styles []*css.ComputedStyleProperty
var r, g, b int
var h int64
2022-12-08 08:55:56 +00:00
var pngCap []byte
2019-08-13 06:35:29 +00:00
chromedp.Run(ctx,
emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), 10, rq.zoom, false),
chromedp.Location(&rq.url),
chromedp.ComputedStyle("body", &styles, chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
2022-11-06 07:51:44 +00:00
_, _, _, _, _, s, err := page.GetLayoutMetrics().Do(ctx)
if err == nil {
h = int64(math.Ceil(s.Height))
}
return nil
}),
)
2024-06-19 07:35:12 +00:00
log.Printf("%s Landed on: %s, Height: %v\n", rq.r.RemoteAddr, rq.url, h)
2019-08-13 06:35:29 +00:00
for _, style := range styles {
if style.Name == "background-color" {
fmt.Sscanf(style.Value, "rgb(%d,%d,%d)", &r, &g, &b)
}
}
height := int64(float64(rq.height) / rq.zoom)
if rq.height == 0 && h > 0 {
2020-10-26 08:49:03 +00:00
height = h + 30
}
2022-11-06 08:47:59 +00:00
chromedp.Run(
ctx, emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), height, rq.zoom, false),
2022-12-08 07:44:09 +00:00
chromedp.Sleep(*delay), // TODO(tenox): find a better way to determine if page is rendered
)
2022-11-06 08:47:59 +00:00
// Capture screenshot...
2022-12-08 08:55:56 +00:00
ctxErr(chromedp.Run(ctx, chromedpCaptureScreenshot(&pngCap, rq.height)), rq.w)
2019-06-26 08:07:13 +00:00
seq := rand.Intn(9999)
2022-12-08 08:55:56 +00:00
imgPath := fmt.Sprintf("/img/%04d.%s", seq, rq.imgType)
mapPath := fmt.Sprintf("/map/%04d.map", seq)
ismap[mapPath] = *rq
var sSize string
var iW, iH int
switch rq.imgType {
2022-12-08 08:56:24 +00:00
case "png":
pngBuf := bytes.NewBuffer(pngCap)
img[imgPath] = *pngBuf
cfg, _, _ := image.DecodeConfig(pngBuf)
sSize = fmt.Sprintf("%.0f KB", float32(len(pngBuf.Bytes()))/1024.0)
iW = cfg.Width
iH = cfg.Height
log.Printf("%s Got PNG image: %s, Size: %s, Res: %dx%d\n", rq.r.RemoteAddr, imgPath, sSize, iW, iH)
2020-10-26 08:32:09 +00:00
case "gif":
2022-12-08 08:55:56 +00:00
i, err := png.Decode(bytes.NewReader(pngCap))
if err != nil {
2022-11-29 13:50:17 +00:00
log.Printf("%s Failed to decode PNG screenshot: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to decode page PNG screenshot:<BR>%s<BR>\n", err)
return
}
2022-11-06 07:27:10 +00:00
st := time.Now()
2022-12-08 08:55:56 +00:00
var gifBuf bytes.Buffer
err = gif.Encode(&gifBuf, gifPalette(i, rq.colors), &gif.Options{})
if err != nil {
2022-11-06 09:11:49 +00:00
log.Printf("%s Failed to encode GIF: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to encode GIF:<BR>%s<BR>\n", err)
return
}
2022-12-08 08:55:56 +00:00
img[imgPath] = gifBuf
sSize = fmt.Sprintf("%.0f KB", float32(len(gifBuf.Bytes()))/1024.0)
iW = i.Bounds().Max.X
iH = i.Bounds().Max.Y
log.Printf("%s Encoded GIF image: %s, Size: %s, Colors: %d, Res: %dx%d, Time: %vms\n", rq.r.RemoteAddr, imgPath, sSize, rq.colors, iW, iH, time.Since(st).Milliseconds())
case "jpg":
i, err := png.Decode(bytes.NewReader(pngCap))
if err != nil {
log.Printf("%s Failed to decode PNG screenshot: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to decode page PNG screenshot:<BR>%s<BR>\n", err)
return
}
st := time.Now()
var jpgBuf bytes.Buffer
err = jpeg.Encode(&jpgBuf, i, &jpeg.Options{Quality: *jpgQual})
if err != nil {
log.Printf("%s Failed to encode JPG: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to encode JPG:<BR>%s<BR>\n", err)
return
}
img[imgPath] = jpgBuf
sSize = fmt.Sprintf("%.0f KB", float32(len(jpgBuf.Bytes()))/1024.0)
iW = i.Bounds().Max.X
iH = i.Bounds().Max.Y
log.Printf("%s Encoded JPG image: %s, Size: %s, Quality: %d, Res: %dx%d, Time: %vms\n", rq.r.RemoteAddr, imgPath, sSize, *jpgQual, iW, iH, time.Since(st).Milliseconds())
}
rq.printHTML(printParams{
2020-10-31 15:51:20 +00:00
bgColor: fmt.Sprintf("#%02X%02X%02X", r, g, b),
pageHeight: fmt.Sprintf("%d PX", h),
2022-12-08 08:55:56 +00:00
imgSize: sSize,
imgURL: imgPath,
mapURL: mapPath,
imgWidth: iW,
imgHeight: iH,
})
2022-11-06 09:11:49 +00:00
log.Printf("%s Done with capture for %s\n", rq.r.RemoteAddr, rq.url)
2019-05-29 08:29:01 +00:00
}
2024-06-22 08:48:46 +00:00
func asciify(s []byte) []byte {
a := make([]byte, len(s))
for i := 0; i < len(s); i++ {
if s[i] > 127 {
a[i] = '.'
continue
}
a[i] = s[i]
}
return a
}
2024-06-22 22:17:26 +00:00
type astTransformer struct{}
func (t *astTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if link, ok := n.(*ast.Link); ok && entering {
link.Destination = append([]byte("?t=txt&url="), link.Destination...)
}
if _, ok := n.(*ast.Image); ok && entering {
// TODO: perhaps instead of deleting images convert them to links
// smaller images or ascii? https://github.com/TheZoraiz/ascii-image-converter
n.Parent().RemoveChildren(n)
}
return ast.WalkContinue, nil
})
}
2024-06-21 08:04:30 +00:00
func (rq *wrpReq) toMarkdown() {
2024-06-22 18:53:20 +00:00
log.Printf("Processing Markdown conversion request for %v", rq.url)
// TODO: bug - DomainFromURL always prefixes with http:// instead of https
2024-06-22 22:17:26 +00:00
// this causes issues on some websites, fix or write a smarter DomainFromURL
2024-06-22 18:53:20 +00:00
c := h2m.NewConverter(h2m.DomainFromURL(rq.url), true, nil)
c.Use(plugin.GitHubFlavored())
2024-06-22 22:17:26 +00:00
md, err := c.ConvertURL(rq.url) // We could also get inner html from chromedp
2024-06-20 06:37:44 +00:00
if err != nil {
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
return
}
2024-06-22 18:53:20 +00:00
log.Printf("Got %v bytes md from %v", len(md), rq.url)
2024-06-22 22:17:26 +00:00
gm := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(parser.WithASTTransformers(util.Prioritized(&astTransformer{}, 100))),
)
var ht bytes.Buffer
err = gm.Convert([]byte(md), &ht)
if err != nil {
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Rendered %v bytes html for %v", len(ht.String()), rq.url)
2024-06-20 06:37:44 +00:00
rq.printHTML(printParams{
2024-06-22 22:17:26 +00:00
text: string(asciify([]byte(ht.String()))),
2024-06-22 08:21:58 +00:00
bgColor: "#FFFFFF",
2024-06-20 06:37:44 +00:00
})
}
// Process HTTP requests to WRP '/' url
2022-11-06 09:11:49 +00:00
func pageServer(w http.ResponseWriter, r *http.Request) {
log.Printf("%s Page Request for %s [%+v]\n", r.RemoteAddr, r.URL.Path, r.URL.RawQuery)
rq := wrpReq{
2022-11-06 09:11:49 +00:00
r: r,
w: w,
}
rq.parseForm()
if len(rq.url) < 4 {
rq.printHTML(printParams{bgColor: "#FFFFFF"})
return
}
2022-12-08 08:28:28 +00:00
rq.navigate() // TODO: if error from navigate do not capture
2024-06-20 06:37:44 +00:00
if rq.imgType == "txt" {
2024-06-21 08:04:30 +00:00
rq.toMarkdown()
2024-06-20 06:37:44 +00:00
return
}
rq.capture()
}
// Process HTTP requests to ISMAP '/map/' url
2022-11-06 09:11:49 +00:00
func mapServer(w http.ResponseWriter, r *http.Request) {
log.Printf("%s ISMAP Request for %s [%+v]\n", r.RemoteAddr, r.URL.Path, r.URL.RawQuery)
rq, ok := ismap[r.URL.Path]
rq.r = r
rq.w = w
if !ok {
2022-11-06 09:11:49 +00:00
fmt.Fprintf(w, "Unable to find map %s\n", r.URL.Path)
log.Printf("Unable to find map %s\n", r.URL.Path)
return
}
2022-12-08 07:44:09 +00:00
if !*noDel {
2022-11-06 09:11:49 +00:00
defer delete(ismap, r.URL.Path)
}
2022-11-06 09:11:49 +00:00
n, err := fmt.Sscanf(r.URL.RawQuery, "%d,%d", &rq.mouseX, &rq.mouseY)
if err != nil || n != 2 {
2022-11-06 09:11:49 +00:00
fmt.Fprintf(w, "n=%d, err=%s\n", n, err)
log.Printf("%s ISMAP n=%d, err=%s\n", r.RemoteAddr, n, err)
return
}
2022-11-06 09:11:49 +00:00
log.Printf("%s WrpReq from ISMAP: %+v\n", r.RemoteAddr, rq)
if len(rq.url) < 4 {
rq.printHTML(printParams{bgColor: "#FFFFFF"})
return
}
2022-12-08 08:28:28 +00:00
rq.navigate() // TODO: if error from navigate do not capture
rq.capture()
}
// Process HTTP requests for images '/img/' url
2022-11-06 09:11:49 +00:00
func imgServer(w http.ResponseWriter, r *http.Request) {
log.Printf("%s IMG Request for %s\n", r.RemoteAddr, r.URL.Path)
2022-12-08 08:55:56 +00:00
imgBuf, ok := img[r.URL.Path]
if !ok || imgBuf.Bytes() == nil {
2022-11-06 09:11:49 +00:00
fmt.Fprintf(w, "Unable to find image %s\n", r.URL.Path)
log.Printf("%s Unable to find image %s\n", r.RemoteAddr, r.URL.Path)
return
}
2022-12-08 07:44:09 +00:00
if !*noDel {
2022-11-06 09:11:49 +00:00
defer delete(img, r.URL.Path)
}
switch {
2022-11-06 09:11:49 +00:00
case strings.HasPrefix(r.URL.Path, ".gif"):
w.Header().Set("Content-Type", "image/gif")
case strings.HasPrefix(r.URL.Path, ".png"):
w.Header().Set("Content-Type", "image/png")
2022-12-08 08:55:56 +00:00
case strings.HasPrefix(r.URL.Path, ".jpg"):
w.Header().Set("Content-Type", "image/jpeg")
}
2022-12-08 08:55:56 +00:00
w.Header().Set("Content-Length", strconv.Itoa(len(imgBuf.Bytes())))
2022-11-06 09:11:49 +00:00
w.Header().Set("Cache-Control", "max-age=0")
w.Header().Set("Expires", "-1")
w.Header().Set("Pragma", "no-cache")
2022-12-08 08:55:56 +00:00
w.Write(imgBuf.Bytes())
2022-11-06 09:11:49 +00:00
w.(http.Flusher).Flush()
}
// Process HTTP requests for Shutdown via '/shutdown/' url
2022-11-06 09:11:49 +00:00
func haltServer(w http.ResponseWriter, r *http.Request) {
log.Printf("%s Shutdown Request for %s\n", r.RemoteAddr, r.URL.Path)
w.Header().Set("Cache-Control", "max-age=0")
w.Header().Set("Expires", "-1")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Shutting down WRP...\n")
w.(http.Flusher).Flush()
time.Sleep(time.Second * 2)
cncl()
acncl()
srv.Shutdown(context.Background())
os.Exit(1)
}
// returns html template, either from html file or built-in
2020-10-29 14:16:14 +00:00
func tmpl(t string) string {
var tmpl []byte
fh, err := os.Open(t)
if err != nil {
2022-11-06 09:12:26 +00:00
goto builtin
2020-10-29 14:16:14 +00:00
}
2022-12-08 07:50:58 +00:00
defer fh.Close()
2020-10-29 14:16:14 +00:00
tmpl, err = ioutil.ReadAll(fh)
if err != nil {
2022-11-06 09:12:26 +00:00
goto builtin
2020-10-29 14:16:14 +00:00
}
2022-12-08 07:50:58 +00:00
log.Printf("Got HTML UI template from %v file, size %v \n", t, len(tmpl))
2020-10-29 14:16:14 +00:00
return string(tmpl)
2022-11-06 09:12:26 +00:00
builtin:
2022-11-30 10:00:53 +00:00
fhs, err := fs.Open("wrp.html")
2020-10-29 14:16:14 +00:00
if err != nil {
log.Fatal(err)
}
2022-12-08 07:50:58 +00:00
defer fhs.Close()
2020-10-29 14:16:14 +00:00
tmpl, err = ioutil.ReadAll(fhs)
if err != nil {
log.Fatal(err)
}
2022-12-08 07:50:58 +00:00
log.Printf("Got HTML UI template from embed\n")
2020-10-29 14:16:14 +00:00
return string(tmpl)
}
2022-12-08 09:49:46 +00:00
// Print my own IP addresses
func printIPs(b string) {
ap := strings.Split(b, ":")
if len(ap) < 1 {
log.Fatal("Wrong format of ipaddress:port")
}
log.Printf("Listen address: %v", b)
if ap[0] != "" && ap[0] != "0.0.0.0" {
return
}
a, err := net.InterfaceAddrs()
if err != nil {
log.Print("Unable to get interfaces: ", err)
return
}
var m string
for _, i := range a {
n, ok := i.(*net.IPNet)
if !ok || n.IP.IsLoopback() || strings.Contains(n.IP.String(), ":") {
continue
}
m = m + n.IP.String() + " "
}
log.Print("My IP addresses: ", m)
}
// Main
2019-05-29 08:29:01 +00:00
func main() {
var err error
2024-05-23 07:34:00 +00:00
log.SetFlags(log.LstdFlags | log.Lshortfile)
2019-05-29 08:52:28 +00:00
flag.Parse()
2024-05-23 07:34:00 +00:00
log.Printf("Web Rendering Proxy Version %s (%v)\n", version, runtime.GOARCH)
2022-12-08 09:49:46 +00:00
log.Printf("Args: %q", os.Args)
if len(os.Getenv("PORT")) > 0 {
2022-12-08 07:44:09 +00:00
*addr = ":" + os.Getenv(("PORT"))
}
2022-12-08 09:49:46 +00:00
printIPs(*addr)
2022-12-08 07:44:09 +00:00
n, err := fmt.Sscanf(*fgeom, "%dx%dx%d", &defGeom.w, &defGeom.h, &defGeom.c)
if err != nil || n != 3 {
log.Fatalf("Unable to parse -g geometry flag / %s", err)
}
opts := append(chromedp.DefaultExecAllocatorOptions[:],
2022-12-08 07:44:09 +00:00
chromedp.Flag("headless", *headless),
2019-07-11 06:58:40 +00:00
chromedp.Flag("hide-scrollbars", false),
2024-01-03 05:55:39 +00:00
chromedp.Flag("enable-automation", false),
chromedp.Flag("disable-blink-features", "AutomationControlled"),
)
2024-01-03 05:54:59 +00:00
if *userAgent != "" {
opts = append(opts, chromedp.UserAgent(*userAgent))
}
actx, acncl = chromedp.NewExecAllocator(context.Background(), opts...)
defer acncl()
ctx, cncl = chromedp.NewContext(actx)
defer cncl()
2019-05-30 09:03:17 +00:00
rand.Seed(time.Now().UnixNano())
2019-07-17 05:29:35 +00:00
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Printf("Interrupt - shutting down.")
cncl()
acncl()
2019-07-17 05:29:35 +00:00
srv.Shutdown(context.Background())
os.Exit(1)
}()
2019-05-29 08:52:28 +00:00
http.HandleFunc("/", pageServer)
2019-06-26 08:07:13 +00:00
http.HandleFunc("/map/", mapServer)
2019-05-30 09:03:17 +00:00
http.HandleFunc("/img/", imgServer)
2019-06-18 06:53:22 +00:00
http.HandleFunc("/shutdown/", haltServer)
2019-05-30 01:47:03 +00:00
http.HandleFunc("/favicon.ico", http.NotFound)
2022-12-08 07:44:09 +00:00
log.Printf("Default Img Type: %v, Geometry: %+v", *defType, defGeom)
2022-12-08 07:44:09 +00:00
htmlTmpl, err = template.New("wrp.html").Parse(tmpl(*htmFnam))
2020-10-29 14:16:14 +00:00
if err != nil {
log.Fatal(err)
}
2022-12-08 09:49:46 +00:00
log.Print("Starting WRP http server")
2022-12-08 07:44:09 +00:00
srv.Addr = *addr
err = srv.ListenAndServe()
2019-06-18 06:53:22 +00:00
if err != nil {
log.Fatal(err)
}
2019-05-29 08:29:01 +00:00
}