mirror of
https://github.com/tenox7/wrp.git
synced 2026-07-09 10:26:46 +00:00
Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
007f7f87d6 | ||
|
|
fb712136b5 | ||
|
|
89426c969d | ||
|
|
e9e97e8146 | ||
|
|
ef43ef5ce6 | ||
|
|
61af9046ae | ||
|
|
90dc64a625 | ||
|
|
534c26698e | ||
|
|
2850d7f3f2 | ||
|
|
7a63e0444f | ||
|
|
6085aaf824 | ||
|
|
a70f1a9ebf | ||
|
|
64c2a16085 | ||
|
|
916b5ee1b7 | ||
|
|
4db89e6a3a | ||
|
|
fc15e4ea44 | ||
|
|
619bd74891 | ||
|
|
fd9e6f3762 | ||
|
|
9c5c495811 | ||
|
|
2457473706 | ||
|
|
79f56a920c | ||
|
|
2042d1bb9d | ||
|
|
ff293f2b7a | ||
|
|
e808b494ab | ||
|
|
88fbe63fb7 | ||
|
|
25b10409db | ||
|
|
5bea0ae9ff | ||
|
|
0d805d82de | ||
|
|
9ccd770e85 | ||
|
|
48ecf6fbe9 | ||
|
|
d4f00119cb | ||
|
|
a87a37f673 | ||
|
|
0fd6967393 | ||
|
|
3ad8f78c45 | ||
|
|
e3dbe85c51 | ||
|
|
30c42bd9b8 | ||
|
|
12724a262e | ||
|
|
25a382e809 | ||
|
|
b03b8a8031 | ||
|
|
a0eb33fe51 | ||
|
|
f56c958aba | ||
|
|
3004962beb | ||
|
|
4d9319eef2 | ||
|
|
7916fa1260 | ||
|
|
9f9014dc15 | ||
|
|
3231a0a61c | ||
|
|
94fb4f437b | ||
|
|
9110ad0853 | ||
|
|
51c4c35651 | ||
|
|
b6e402029a | ||
|
|
9f7107c00b | ||
|
|
500ad0d19a | ||
|
|
b5747f52e7 | ||
|
|
0d998af68c | ||
|
|
eb38499280 | ||
|
|
56fa314d61 | ||
|
|
335a84f52e | ||
|
|
bb29ce38de | ||
|
|
db4ed0d811 | ||
|
|
2f667e447c | ||
|
|
00304b5d05 | ||
|
|
9f21d8d06e | ||
|
|
b0f4170c6c | ||
|
|
8d165df36d | ||
|
|
407907bece | ||
|
|
f838caa328 | ||
|
|
a8b8a557f7 | ||
|
|
d036914fcb | ||
|
|
701240eb7c | ||
|
|
1d9af26604 | ||
|
|
5ed55c387c | ||
|
|
0e3192b69f | ||
|
|
91870f5724 | ||
|
|
c1ffd71e25 | ||
|
|
26b3c21aa7 | ||
|
|
8cec22eb90 | ||
|
|
e64f413c76 | ||
|
|
94d0e0d128 | ||
|
|
b546b5cbae | ||
|
|
a2a4152b86 | ||
|
|
db77479bad | ||
|
|
33436333cf | ||
|
|
866750700b | ||
|
|
3f445abd14 | ||
|
|
d7eb89e135 | ||
|
|
bf946f0f63 | ||
|
|
7673e15b9e | ||
|
|
d00b1d5d7f | ||
|
|
3e8f109edd | ||
|
|
9a0e5809c1 | ||
|
|
96acf94521 | ||
|
|
807373d668 | ||
|
|
703e9d0452 | ||
|
|
f7799ecba5 | ||
|
|
37562cce23 | ||
|
|
631971b0bb | ||
|
|
23a7ba5cbf | ||
|
|
2733a84d2d | ||
|
|
abea41f498 | ||
|
|
da524ff275 | ||
|
|
b92478fd6a | ||
|
|
ee0e72f246 | ||
|
|
a3c06d346c | ||
|
|
0065fdc2c7 | ||
|
|
22efadf9cf | ||
|
|
4070fa7a53 | ||
|
|
41a14ad590 | ||
|
|
a212a14bd8 | ||
|
|
5a86c32d9a | ||
|
|
55f3c852f9 | ||
|
|
79c86a7056 | ||
|
|
19f4be3ac1 | ||
|
|
6ffdf1fd7f | ||
|
|
0796c86e73 | ||
|
|
d8b4f160ac | ||
|
|
93e9fddca7 | ||
|
|
e13a06610b | ||
|
|
5b28f1d913 | ||
|
|
da5246b9df | ||
|
|
8a484813c3 | ||
|
|
75233ccb97 | ||
|
|
04954f8be6 | ||
|
|
884374edcd | ||
|
|
ba522e3a86 | ||
|
|
e05135e433 | ||
|
|
67d550672b | ||
|
|
71889effe7 | ||
|
|
6a7ffbb67b | ||
|
|
f93058fed3 | ||
|
|
67ed26b8f4 | ||
|
|
6aefcdacc5 | ||
|
|
de315fb95f | ||
|
|
b098b3632a | ||
|
|
5ec0266f75 | ||
|
|
7d9cec6297 | ||
|
|
43df501d97 | ||
|
|
85d64a3577 | ||
|
|
993e5723ed | ||
|
|
9056c798d6 | ||
|
|
b937bea370 | ||
|
|
8fd65b3e47 | ||
|
|
8716a983ce | ||
|
|
4757cfd32b | ||
|
|
79e6f3a17e | ||
|
|
d8f5c6fb28 | ||
|
|
c816ef712a | ||
|
|
d28924583c | ||
|
|
1c17b39ea5 | ||
|
|
ecb2cc0c06 | ||
|
|
b571df7a37 | ||
|
|
78c568ac09 | ||
|
|
04a755749e | ||
|
|
e983f244f8 | ||
|
|
9b76c045d6 | ||
|
|
c8e391274a | ||
|
|
7b1274b9d4 | ||
|
|
0128b3ff8e | ||
|
|
6de3fad580 | ||
|
|
d7dcb58adc | ||
|
|
e5f83225f7 | ||
|
|
36803c4312 | ||
|
|
40e081be77 | ||
|
|
1c18fb9b81 | ||
|
|
363fbcd225 | ||
|
|
bf7e7bfb2c | ||
|
|
fec812bc32 | ||
|
|
a238a0ea6f | ||
|
|
444b7b31d7 | ||
|
|
76e72d5368 | ||
|
|
c4d9833707 | ||
|
|
9cd286add8 | ||
|
|
fdfbe80024 | ||
|
|
d602124ed6 | ||
|
|
6e5e829b02 | ||
|
|
f978d91ba9 | ||
|
|
9b15feacb2 | ||
|
|
08a89c6097 | ||
|
|
fa3bd3f8fb | ||
|
|
55f4e45b4c | ||
|
|
9e77aa7261 | ||
|
|
8a9870d8e2 | ||
|
|
7c33bc67dc | ||
|
|
48c4ab8254 | ||
|
|
bc5f8cabb1 | ||
|
|
cc16d6c3b9 | ||
|
|
1d26a451ea | ||
|
|
cfb608c1f3 | ||
|
|
d1fcd30db8 | ||
|
|
c7811a6886 | ||
|
|
3b88b8665b | ||
|
|
ad84f6d087 | ||
|
|
97d1443d8c | ||
|
|
82bbd4bdaa | ||
|
|
36d0cdcb0a | ||
|
|
f5be172d43 | ||
|
|
cf8b85e15a | ||
|
|
700c4aa495 | ||
|
|
f05dde8188 | ||
|
|
2a22cfd755 | ||
|
|
56ac414405 | ||
|
|
a79f477948 | ||
|
|
c036841c0a | ||
|
|
878f43af75 | ||
|
|
b54ebbf9e5 | ||
|
|
5dc4699ac9 | ||
|
|
f6e1f3ee88 | ||
|
|
8aaf435225 | ||
|
|
4fa913a9dd | ||
|
|
4302731bc8 | ||
|
|
d6b33ad140 | ||
|
|
3dddb70be0 | ||
|
|
3ff226e1df | ||
|
|
1ddf005a23 | ||
|
|
69efa1fb92 | ||
|
|
2c2fbd11a6 | ||
|
|
259f998787 | ||
|
|
1ab9124a4f | ||
|
|
4d0c8b9e7e | ||
|
|
fa25e816a7 | ||
|
|
36427fac64 | ||
|
|
ac594cdebd | ||
|
|
11b5ce9b6d | ||
|
|
b8ae1ceba5 | ||
|
|
3224c63fd1 | ||
|
|
733be4a14a | ||
|
|
4533e38a31 | ||
|
|
d4043f0b7d | ||
|
|
c64380dd72 | ||
|
|
4d911cb330 | ||
|
|
a3beaf4b14 | ||
|
|
311bb829da | ||
|
|
41dfa7dae2 | ||
|
|
889561aeb0 | ||
|
|
b90300ba2d | ||
|
|
c80cb876ce | ||
|
|
ef04d2da72 | ||
|
|
2e9773f705 | ||
|
|
0957fedaee | ||
|
|
60ca1a0d50 | ||
|
|
0c728b08fe | ||
|
|
81b47eb59c | ||
|
|
15ebf497b8 | ||
|
|
9215ed57c0 | ||
|
|
ba0b521762 | ||
|
|
34b25be7d7 | ||
|
|
c4e3671468 | ||
|
|
f73c778b7c | ||
|
|
c9cedb7f81 | ||
|
|
f69a6e5219 | ||
|
|
a258f603b3 | ||
|
|
b30458930b | ||
|
|
9fca2704dc | ||
|
|
62b11cb216 | ||
|
|
78f9598af5 | ||
|
|
5ce1c2456f | ||
|
|
c93c2c883e | ||
|
|
d7a47d366b | ||
|
|
ffcaca4907 | ||
|
|
260840adb5 | ||
|
|
fcd746aa9a | ||
|
|
e2c06b2e7b | ||
|
|
fafe232463 | ||
|
|
6c49b1f73c | ||
|
|
a533521784 | ||
|
|
0f9ebc6252 | ||
|
|
af5174456a | ||
|
|
d49ef9c1c2 | ||
|
|
23b4fbaf63 | ||
|
|
a91cc60a51 | ||
|
|
51cd108bad | ||
|
|
cd2cf0baae | ||
|
|
a344d177d6 | ||
|
|
02766d8844 | ||
|
|
91091cf94b | ||
|
|
95d9de7348 | ||
|
|
6449c64e36 | ||
|
|
b058831ec6 | ||
|
|
7c50c6e841 | ||
|
|
2f2e99eb85 | ||
|
|
4dee5ea8d9 | ||
|
|
333666d3b0 | ||
|
|
780143b766 | ||
|
|
6b89e463f3 | ||
|
|
ea1ae10f97 | ||
|
|
eb4201c56b | ||
|
|
4cd55b31b0 | ||
|
|
f0ba852785 | ||
|
|
66412fa95e | ||
|
|
cd5bb94def | ||
|
|
357f3ed6bf | ||
|
|
97c0679e0b | ||
|
|
e2223af833 | ||
|
|
60989d3395 | ||
|
|
74d015a4da | ||
|
|
8628c00dd7 | ||
|
|
327baf318a | ||
|
|
0e07f422f6 | ||
|
|
f7aece10e9 | ||
|
|
a7b7164932 | ||
|
|
7a27cf7b62 | ||
|
|
749f8bea5d | ||
|
|
290bc5a977 | ||
|
|
cc98932f5a | ||
|
|
f69e213a0b | ||
|
|
66641a099b | ||
|
|
872321c699 | ||
|
|
ba4183e0b4 | ||
|
|
4212678d81 | ||
|
|
0d2ba9d4b2 | ||
|
|
a63d4ef50d | ||
|
|
127114f753 | ||
|
|
b313e703fb | ||
|
|
ee26c40eb3 | ||
|
|
6c29008eb5 | ||
|
|
bb84d43d31 | ||
|
|
7067d2cdf8 | ||
|
|
640a405622 | ||
|
|
22dd6aaab2 | ||
|
|
d102016ba9 | ||
|
|
253fef2aad | ||
|
|
fdfad6bc69 | ||
|
|
1807790629 | ||
|
|
bd7d92393d | ||
|
|
e3b28e93c5 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
wrp-*
|
||||
wrp
|
||||
wrp.exe
|
||||
statik
|
||||
92
.goreleaser.yml
Normal file
92
.goreleaser.yml
Normal file
@@ -0,0 +1,92 @@
|
||||
version: 2
|
||||
|
||||
project_name: wrp
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- main: .
|
||||
binary: wrp
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- freebsd
|
||||
- openbsd
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
goarm:
|
||||
- "7"
|
||||
ldflags:
|
||||
- -s -w
|
||||
|
||||
archives:
|
||||
- formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
- zip
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- "tenox7/wrp:{{ .Tag }}-amd64"
|
||||
- "tenox7/wrp:latest-amd64"
|
||||
use: buildx
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.goreleaser
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
|
||||
- image_templates:
|
||||
- "tenox7/wrp:{{ .Tag }}-arm64"
|
||||
- "tenox7/wrp:latest-arm64"
|
||||
use: buildx
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.goreleaser
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
|
||||
docker_manifests:
|
||||
- name_template: "tenox7/wrp:{{ .Tag }}"
|
||||
image_templates:
|
||||
- "tenox7/wrp:{{ .Tag }}-amd64"
|
||||
- "tenox7/wrp:{{ .Tag }}-arm64"
|
||||
|
||||
- name_template: "tenox7/wrp:latest"
|
||||
image_templates:
|
||||
- "tenox7/wrp:latest-amd64"
|
||||
- "tenox7/wrp:latest-arm64"
|
||||
|
||||
nfpms:
|
||||
- package_name: wrp
|
||||
vendor: Antoni Sawicki
|
||||
homepage: https://github.com/tenox7/wrp
|
||||
maintainer: Antoni Sawicki <as@tenoware.com>
|
||||
description: Web Rendering Proxy
|
||||
license: Apache-2.0
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
- apk
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: tenox7
|
||||
name: wrp
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM golang AS builder
|
||||
WORKDIR /src
|
||||
RUN git clone https://github.com/tenox7/wrp.git
|
||||
WORKDIR /src/wrp
|
||||
RUN go mod download
|
||||
ARG TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -o /wrp-${TARGETARCH}
|
||||
|
||||
FROM chromedp/headless-shell
|
||||
ARG TARGETARCH
|
||||
COPY --from=builder /wrp-${TARGETARCH} /wrp
|
||||
ENTRYPOINT ["/wrp"]
|
||||
ENV PATH="/headless-shell:${PATH}"
|
||||
LABEL maintainer="as@tenoware.com"
|
||||
5
Dockerfile.goreleaser
Normal file
5
Dockerfile.goreleaser
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM chromedp/headless-shell
|
||||
COPY wrp /wrp
|
||||
ENTRYPOINT ["/wrp"]
|
||||
ENV PATH="/headless-shell:${PATH}"
|
||||
LABEL maintainer="as@tenoware.com"
|
||||
6
Dockerfile.local
Normal file
6
Dockerfile.local
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM chromedp/headless-shell
|
||||
ARG TARGETARCH
|
||||
ADD wrp-${TARGETARCH}-linux /wrp
|
||||
ENTRYPOINT ["/wrp"]
|
||||
ENV PATH="/headless-shell:${PATH}"
|
||||
LABEL maintainer="as@tenoware.com"
|
||||
39
Makefile
Normal file → Executable file
39
Makefile
Normal file → Executable file
@@ -1,25 +1,28 @@
|
||||
all: linux freebsd openbsd macos windows rpi
|
||||
all: wrp
|
||||
|
||||
linux:
|
||||
GOOS=linux GOARCH=amd64 go build -a -o wrp-linux wrp.go
|
||||
wrp: wrp.go
|
||||
go build -a
|
||||
|
||||
linux-ppc64le:
|
||||
GOOS=linux GOARCH=ppc64le go build -a -o wrp-linux-ppc64le wrp.go
|
||||
cross:
|
||||
GOOS=linux GOARCH=amd64 go build -a -o wrp-amd64-linux
|
||||
GOOS=freebsd GOARCH=amd64 go build -a -o wrp-amd64-freebsd
|
||||
GOOS=openbsd GOARCH=amd64 go build -a -o wrp-amd64-openbsd
|
||||
GOOS=darwin GOARCH=amd64 go build -a -o wrp-amd64-macos
|
||||
GOOS=darwin GOARCH=arm64 go build -a -o wrp-arm64-macos
|
||||
GOOS=windows GOARCH=amd64 go build -a -o wrp-amd64-windows.exe
|
||||
GOOS=linux GOARCH=arm go build -a -o wrp-arm-linux
|
||||
GOOS=linux GOARCH=arm64 go build -a -o wrp-arm64-linux
|
||||
|
||||
rpi:
|
||||
GOOS=linux GOARCH=arm go build -a -o wrp-linux-rpi wrp.go
|
||||
docker-local:
|
||||
GOOS=linux GOARCH=amd64 go build -a -o wrp-amd64-linux
|
||||
GOOS=linux GOARCH=arm64 go build -a -o wrp-arm64-linux
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t tenox7/wrp:latest -f Dockerfile.local --load .
|
||||
|
||||
freebsd:
|
||||
GOOS=freebsd GOARCH=amd64 go build -a -o wrp-freebsd wrp.go
|
||||
docker-push:
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t tenox7/wrp:latest --push .
|
||||
|
||||
openbsd:
|
||||
GOOS=openbsd GOARCH=amd64 go build -a -o wrp-openbsd wrp.go
|
||||
|
||||
macos:
|
||||
GOOS=darwin GOARCH=amd64 go build -a -o wrp-macos wrp.go
|
||||
|
||||
windows:
|
||||
GOOS=windows GOARCH=amd64 go build -a -o wrp-windows.exe wrp.go
|
||||
docker-clean:
|
||||
docker buildx prune -a -f
|
||||
|
||||
clean:
|
||||
rm -rf wrp-linux wrp-freebsd wrp-openbsd wrp-macos wrp-windows.exe wrp-linux-rpi
|
||||
rm -rf wrp-* wrp
|
||||
|
||||
246
README.md
246
README.md
@@ -1,42 +1,240 @@
|
||||
# WRP - Web Rendering Proxy
|
||||
|
||||
A HTTP proxy server that allows to use historical and obsolete web browsers on the modern web. It works by rendering the web page in to a GIF image. It sends mouse clicks via ISMAP and keystrokes from a text box form input.
|
||||
A browser-in-browser proxy server that allows to use historical / vintage web browsers on the modern web.
|
||||
|
||||
## Current Status
|
||||
## Modes of operation
|
||||
|
||||
* This is a new reimplementation in GoLang/[ChromeDP](https://github.com/chromedp/chromedp). Python/Webkit being now deprecated.
|
||||
* Beta but fully supported an maintained.
|
||||
* Works as browser-in-browser. A real http proxy mode is being investigated. Check [issue #35](https://github.com/tenox7/wrp/issues/35) for updates.
|
||||
* As of 4.1 supports clicking on non-link elements (eg. cookie warnings, dropdown menus, etc.) and sending keystrokes. Yes, you can login and use Gmail or play web based games from any old browser.
|
||||
### Access
|
||||
|
||||
## Usage
|
||||
- Browser in browser, navigate to the address of WRP and use built-in web form to naviate.
|
||||
- HTTP proxy mode which offers more seamless, but also limited use.
|
||||
|
||||
1. [Download a WRP binary](https://github.com/tenox7/wrp/releases/) and run it on a machine that will become your WRP server.
|
||||
2. Point your legacy browser to `http://address:port` of WRP server. Do not set or use it as a "Proxy Server" (yet).
|
||||
3. Type a search string or a http/https URL and click GO.
|
||||
4. Adjust your screen width/height/scale/#colors to fit in your old browser.
|
||||
5. Scroll web page by clicking on the in-image scroll bar.
|
||||
6. Send keystrokes by filling in K input box and pressing Go. You also have buttons for backspace, enter and arrow keys.
|
||||
### Rende modes
|
||||
|
||||
- ISMAP "graphical" mode, renders web page in to a GIF/PNG/JPG image with clickable imagemap.
|
||||
- Simple HTML mode converts pages into simplified HTML for old browsers.
|
||||
|
||||

|
||||
|
||||
## Usage Instructions
|
||||
|
||||
* [Download a WRP binary](https://github.com/tenox7/wrp/releases/) run it on a machine that will become your WRP gateway/server. This should be modern hardware and OS. Google Chrome / Chromium Browser is required to be preinstalled. Do not try to run WRP on an old machine like Windows XP or 98.
|
||||
* Make sure you have disabled firewall or open port WRP is listening on (by default 8080).
|
||||
* Point your legacy browser to `http://address:port` of the WRP server or set your proxy address there.
|
||||
* Type a search string or a full http/https URL and click **Go**.
|
||||
* Select whether you want to use graphical (ISMAP) or simple HTML mode.
|
||||
|
||||
### Proxy mode
|
||||
|
||||
- Try to use http:// addresses only, the proxy will automatically rewrite them to https as needed.
|
||||
- https:// addresses are limited in functionality (no /suffix or path).
|
||||
- You can use `/proxy.pac` automatic configuration mode.
|
||||
- Proxy mode doesn't have a customizable web interface with html forms. All configuration is done via flags.
|
||||
|
||||
### Image Map Mode
|
||||
|
||||
* Adjust your screen **W**idth/**H**eight/**S**cale/**C**olors to fit in your old browser.
|
||||
* Scroll web page by clicking on the in-image scroll bar on the right.
|
||||
* WRP also allows **a single tall image without the vertical scrollbar** and use client scrolling. To enable this, simply height **H** to `0` (or flag `-g 1152x0x216`. However this should not be used with old and low spec clients. Such tall images will be very large, take a lot of memory and long time to process, especially for GIFs.
|
||||
* Do not use client browser history-back, instead use **Bk** button in the app.
|
||||
* You can re-capture screenshot without reloading page by using **St** (Stop). This is useful if page didn't render fully before screenshot is taken.
|
||||
* You can also reload page and re-capture screenshot with **Re** (Reload).
|
||||
* To send keystrokes, fill **K** input box and press **Go**. There also are buttons for backspace, enter and arrow keys.
|
||||
* The default image type GIP is a ultra fast, optimized, parallel encoded GIF type.
|
||||
* If your browser supports it, prefer PNG over GIF/JPG. PNG is much faster, whereas GIF/JPG requires a lot of additional processing on both client and server to encode/decode.
|
||||
* GIF images are by default encoded with 216 colors, "web safe" palette. This uses an ultra fast but not very accurate color mapping algorithm. If you want better color representation switch to 256 color mode.
|
||||
|
||||
### Simple HTML mode
|
||||
|
||||
* Select image type PNG/GIF/JPG. Each individual image from the original web site will be converted to the selected format.
|
||||
* Type maximum image size in pixels.
|
||||
|
||||
## UI explanation
|
||||
|
||||
The first unnamed input box is either search (google) or URL starting with http/https
|
||||
|
||||
`Go` Navigate to the url or perform search
|
||||
|
||||
`Bk` History Back
|
||||
|
||||
`St` Stop, also re-capture screenshot without refreshing page, for example if page
|
||||
render takes a long time or it updates / changes periodically
|
||||
|
||||
`Re` Remote Reload / Refresh
|
||||
|
||||
`Up` Page Up
|
||||
|
||||
`Dn` Page Down
|
||||
|
||||
`W` is width in pixels, adjust it to get rid of horizontal scroll bar
|
||||
|
||||
`H` is height in pixels, adjust it to get rid of vertical scroll bar.
|
||||
It can also be set to 0 to produce one very tall image and use
|
||||
client scroll. This 0 size is experimental, buggy and should be
|
||||
used with PNG and lots of memory on a client side.
|
||||
|
||||
`Z` Zoom or scale
|
||||
|
||||
`M` Mode - ISMAP (clickable imagemap) or simple HTML mode
|
||||
|
||||
`T` Image type PNG / GIF / JPEG
|
||||
|
||||
`C` Colors, for GIF images only
|
||||
|
||||
`K` Keystroke input, you can type some letters in it and when you click Go it will be typed in the remote browser.
|
||||
|
||||
`Bs` Backspace
|
||||
|
||||
`Rt` Return / enter
|
||||
|
||||
### UI Customization
|
||||
|
||||
WRP supports customizing it's own UI using HTML Template file. Download [wrp.html](wrp.html) place in the same directory with wrp binary customize it to your liking.
|
||||
|
||||
## Docker
|
||||
|
||||
https://hub.docker.com/r/tenox7/wrp
|
||||
|
||||
```shell
|
||||
$ docker run -d --rm -p 8080:8080 tenox7/wrp:latest
|
||||
```
|
||||
|
||||
## AWS
|
||||
|
||||
It's possible to run WRP on AWS App Runner.
|
||||
|
||||
First you need to upload the Docker image to ECR - [Instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html).
|
||||
|
||||
Create App Runner service using the uploaded image using the AWS Console or CLI.
|
||||
|
||||
[AWS Console](https://console.aws.amazon.com/apprunner/home#/create)
|
||||
|
||||
```shell
|
||||
aws apprunner create-service --service-name my-app-runner-service --source-configuration '{
|
||||
"ImageRepository": {
|
||||
"ImageIdentifier": "<account_id>.dkr.ecr.<region>.amazonaws.com/wrp:latest",
|
||||
"ImageRepositoryType": "ECR",
|
||||
"ImageConfiguration": {"Port": "8000"},
|
||||
"AutoDeploymentsEnabled": true
|
||||
}
|
||||
}' --instance-configuration '{
|
||||
"Cpu": "1024",
|
||||
"Memory": "2048",
|
||||
"InstanceRoleArn": "arn:aws:iam::<account_id>:role/AppRunnerECRAccessRole"
|
||||
}'
|
||||
```
|
||||
|
||||
## Azure Container Instances
|
||||
|
||||
[Azure Console](https://portal.azure.com/#create/Microsoft.ContainerInstances)
|
||||
|
||||
CLI:
|
||||
|
||||
```shell
|
||||
$ az container create --resource-group wrp --name wrp --image tenox7/wrp:latest --cpu 1 --memory 2 --ports 80 --protocol tcp --os-type Linux --ip-address Public --command-line '/wrp -l :80 -t png -g 1280x0x256'
|
||||
```
|
||||
|
||||
## Google Cloud Run
|
||||
|
||||
```shell
|
||||
$ gcloud run deploy --platform managed --image=tenox7/wrp:latest --memory=2Gi --args='-t=png','-g=1280x0x256'
|
||||
```
|
||||
|
||||
Unfortunately Google Cloud Run forces you to use HTTPS, which likely won't work with old browsers.
|
||||
|
||||
|
||||
## Flags
|
||||
```
|
||||
-l listen address:port, default :8080
|
||||
-h headed mode, display browser window
|
||||
-d chromedp debug logging
|
||||
|
||||
```text
|
||||
-l listen address:port (default :8080)
|
||||
-m mode, either ismap (graphical) or html
|
||||
-t image type gif, png or jpg (default gif)
|
||||
-g image geometry, WxHxC, height can be 0 for unlimited (default 1152x600x216)
|
||||
C (number of colors) is only used for GIF
|
||||
-q Jpeg image quality, default 75%
|
||||
-h headless mode, hide browser window on the server (default true)
|
||||
-n do not free maps and images after use (default false)
|
||||
-ui html template file (default "wrp.html")
|
||||
-ua user agent, override the default "headless" agent (only for ismap mode)
|
||||
-s delay/sleep after page is rendered before screenshot is taken (default 2s)
|
||||
-b browser executable path (e.g., for Brave Browser)
|
||||
```
|
||||
|
||||
## More info and screenshots
|
||||
* http://virtuallyfun.superglobalmegacorp.com/2014/03/11/web-rendering-proxy-update/
|
||||
* http://virtuallyfun.superglobalmegacorp.com/2014/03/03/surfing-modern-web-with-ancient-browsers/
|
||||
## Minimal Requirements
|
||||
|
||||
* Server/Gateway requires modern hardware and operating system that is supported by [Go language](https://github.com/golang/go/wiki/MinimumRequirements) and Chrome/Chromium Browser, which must be installed.
|
||||
* Client Browser needs to support `HTML FORMs` and `ISMAP`. Typically [Mosaic 2.0](http://www.ncsa.illinois.edu/enabling/mosaic/versions) would be minimum version for forms. However ISMAP was supported since 0.6B, so if you manually enter url using `?url=...`, you can use the earlier version.
|
||||
|
||||
## FAQ
|
||||
|
||||
### I can't get it to run
|
||||
|
||||
This program does not have a GUI and is run from the command line. After downloading, you may need to enable executable bit on Unix systems, for example:
|
||||
|
||||
```shell
|
||||
$ cd ~/Downloads
|
||||
$ chmod +x wrp-amd64-macos
|
||||
$ ./wrp-amd64-macos
|
||||
```
|
||||
|
||||
### Pages are chopped off
|
||||
|
||||
Click `st` to re-capture screenshot. You may want to increase the page delay using `-s` flag.
|
||||
|
||||
### Websites are blocking headless browsers
|
||||
|
||||
This is a well known issue. WRP has some provisions to work around it, but it's a cat and mouse game. By default WRP tries to obtain some current valid User Agent
|
||||
from https://github.com/jnrbsn/user-agents rather than using the internal "HeadlessChrome". You can override this to your own, for example:
|
||||
|
||||
```shell
|
||||
$ wrp -ua="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
|
||||
```
|
||||
|
||||
### Why isn't there a Docker image for armv6
|
||||
|
||||
Because https://hub.docker.com/r/chromedp/headless-shell/ doesn't have one. WRP uses that image. If you have a fork that builds for armv6 let me know.
|
||||
|
||||
### WTF is GIP image format
|
||||
|
||||
It's just GIF but optimized. Avoids dithering, uses fast color palette and parallel encoding. https://github.com/tenox7/gip
|
||||
|
||||
## History
|
||||
|
||||
* Version 1.0 (2014) started as a *cgi-bin* script, adaptation of `webkit2png.py` and `pcidade.py`, [blog post](https://virtuallyfun.com/2014/03/03/surfing-modern-web-with-ancient-browsers/).
|
||||
* Version 2.0 became a stand alone http-proxy server, supporting both Linux and MacOS, [another post](https://virtuallyfun.com/wordpress/2014/03/11/web-rendering-proxy-update//).
|
||||
* In 2016 thanks to [Let's Encrypt](https://en.wikipedia.org/wiki/Let%27s_Encrypt) the whole internet migrated to HTTPS/SSL/TLS and WRP largely stopped working. Python code became unmaintainable and there was no easy way to make it work on Windows, even under WSL.
|
||||
* Version 3.0 (2019) has been rewritten in [Go](https://golang.org/) using [Chromedp](https://github.com/chromedp) as browser-in-browser instead of http-proxy. The initial version was [less than 100 lines of code](https://gist.github.com/tenox7/b0f03c039b0a8b67f6c1bf47e2dd0df0).
|
||||
* Version 4.0 has been completely refactored to use mouse clicks via imagemap instead parsing a href nodes.
|
||||
* Version 4.1 added sending keystrokes in to input boxes. You can now login to Gmail. Also now runs as a Docker container and on Cloud Run/Azure Containers.
|
||||
* Version 4.5 introduces rendering whole pages in to a single tall image with client scrolling.
|
||||
* Version 4.6 adds blazing fast gif encoding by [Hill Ma](https://github.com/mahiuchun).
|
||||
* Version 4.6.3 adds arm64 / aarch64 Docker container support - you can run it on Raspberry PI!
|
||||
* Version 4.7 add simple html aka reader aka text mode.
|
||||
* Version 4.8 add image support to simple html mode.
|
||||
* Version 4.9 adds support for ultra fast, parallel encoded gif image (GIP)
|
||||
* Version 4.10 re-adds support for http proxy mode
|
||||
|
||||
## Credits
|
||||
|
||||
## Credits
|
||||
* Uses [chromedp](https://github.com/chromedp), thanks to [mvdan](https://github.com/mvdan) for dealing with my issues
|
||||
* Uses [go-quantize](https://github.com/ericpauley/go-quantize), thanks to [ericpauley](https://github.com/ericpauley) for developing the missing go quantizer
|
||||
* Thanks to Jason Stevens of [Fun With Virtualization](https://virtuallyfun.com/) for graciously hosting my rumblings
|
||||
* Thanks to [claunia](https://github.com/claunia/) for help with the Python/Webkit version in the past
|
||||
* Thanks to [Hill Ma](https://github.com/mahiuchun) for ultra fast gif encoding algorithm
|
||||
* Historical Python/Webkit versions and prior art can be seen in [wrp-old](https://github.com/tenox7/wrp-old) repo
|
||||
|
||||
## Related
|
||||
|
||||
You may also be interested in:
|
||||
|
||||
* [VncFox](https://github.com/tenox7/vncfox)
|
||||
* [webone](https://github.com/atauenis/webone)
|
||||
* [Browservice](https://github.com/ttalvitie/browservice)
|
||||
* [Browsh](https://github.com/browsh-org/browsh)
|
||||
|
||||
## Legal Stuff
|
||||
License: Apache 2.0
|
||||
Copyright (c) 2013-2018 Antoni Sawicki
|
||||
Copyright (c) 2019 Google LLC
|
||||
|
||||
```text
|
||||
License: Apache 2.0
|
||||
Copyright (c) 2013-2026 Antoni Sawicki
|
||||
```
|
||||
|
||||
31
go.mod
Normal file
31
go.mod
Normal file
@@ -0,0 +1,31 @@
|
||||
module github.com/tenox7/wrp
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/MaxHalford/halfgone v0.0.0-20171017091812-482157b86ccb
|
||||
github.com/PuerkitoBio/goquery v1.11.0
|
||||
github.com/breml/rootcerts v0.3.4
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
|
||||
github.com/chromedp/chromedp v0.14.2
|
||||
github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
||||
github.com/tenox7/gip v1.0.1
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/net v0.51.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
)
|
||||
114
go.sum
Normal file
114
go.sum
Normal file
@@ -0,0 +1,114 @@
|
||||
github.com/MaxHalford/halfgone v0.0.0-20171017091812-482157b86ccb h1:YQ+d0g0P0F/06oDoeEgDHeZCIrnKgLxXcqYOpe8sTuU=
|
||||
github.com/MaxHalford/halfgone v0.0.0-20171017091812-482157b86ccb/go.mod h1:J86XzS1wgzJPjpQmpriJ+SetP17JSQUd9l+HWQK86jA=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys=
|
||||
github.com/breml/rootcerts v0.3.4/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
|
||||
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU=
|
||||
github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/tenox7/gip v1.0.1 h1:yRcHROzwBjV2BhCjnh1y19wIg5Ei5CTMaZ+lx9nMl3Q=
|
||||
github.com/tenox7/gip v1.0.1/go.mod h1:MR/eaUKjLGkYIguDcAUrWyxG58ipjjCrzM92jwGqDno=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
420
ismap.go
Normal file
420
ismap.go
Normal file
@@ -0,0 +1,420 @@
|
||||
// WRP ISMAP / ChromeDP routines
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/input"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/tenox7/gip"
|
||||
)
|
||||
|
||||
type cachedImg struct {
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
type cachedMap struct {
|
||||
req wrpReq
|
||||
}
|
||||
|
||||
type wrpCache struct {
|
||||
sync.Mutex
|
||||
imgs map[string]cachedImg
|
||||
maps map[string]cachedMap
|
||||
}
|
||||
|
||||
func (c *wrpCache) addImg(path string, buf bytes.Buffer) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.imgs[path] = cachedImg{buf: buf}
|
||||
}
|
||||
|
||||
func (c *wrpCache) getImg(path string) (bytes.Buffer, bool) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
e, ok := c.imgs[path]
|
||||
if !ok {
|
||||
return bytes.Buffer{}, false
|
||||
}
|
||||
return e.buf, true
|
||||
}
|
||||
|
||||
func (c *wrpCache) addMap(path string, req wrpReq) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.maps[path] = cachedMap{req: req}
|
||||
}
|
||||
|
||||
func (c *wrpCache) getMap(path string) (wrpReq, bool) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
e, ok := c.maps[path]
|
||||
if !ok {
|
||||
return wrpReq{}, false
|
||||
}
|
||||
return e.req, true
|
||||
}
|
||||
|
||||
func (c *wrpCache) clear() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.imgs = make(map[string]cachedImg)
|
||||
c.maps = make(map[string]cachedMap)
|
||||
}
|
||||
|
||||
func chromedpStart() (context.CancelFunc, context.CancelFunc) {
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", *headless),
|
||||
chromedp.Flag("hide-scrollbars", false),
|
||||
chromedp.Flag("enable-automation", false),
|
||||
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
||||
)
|
||||
if *userAgent == "jnrbsn" {
|
||||
if ua := fetchJnrbsnUserAgent(); ua != "" {
|
||||
*userAgent = ua
|
||||
}
|
||||
}
|
||||
if *userAgent != "" {
|
||||
opts = append(opts, chromedp.UserAgent(*userAgent))
|
||||
}
|
||||
if *browserPath != "" {
|
||||
opts = append(opts, chromedp.ExecPath(*browserPath))
|
||||
}
|
||||
if *userDataDir != "" {
|
||||
opts = append(opts, chromedp.UserDataDir(*userDataDir))
|
||||
}
|
||||
actx, acncl = chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
ctx, cncl = chromedp.NewContext(actx)
|
||||
return cncl, acncl
|
||||
}
|
||||
|
||||
// Determine what action to take
|
||||
func (rq *wrpReq) action() chromedp.Action {
|
||||
// Mouse Click
|
||||
if rq.mouseX > 0 && rq.mouseY > 0 {
|
||||
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))
|
||||
}
|
||||
// Buttons
|
||||
if len(rq.buttons) > 0 {
|
||||
log.Printf("%s Button %v\n", rq.r.RemoteAddr, rq.buttons)
|
||||
switch rq.buttons {
|
||||
case "Bk":
|
||||
return chromedp.NavigateBack()
|
||||
case "St":
|
||||
return chromedp.Stop()
|
||||
case "Re":
|
||||
return chromedp.Reload()
|
||||
case "Bs":
|
||||
return chromedp.KeyEvent("\b")
|
||||
case "Rt":
|
||||
return chromedp.KeyEvent("\r")
|
||||
case "<":
|
||||
return chromedp.KeyEvent("\u0302")
|
||||
case "^":
|
||||
return chromedp.KeyEvent("\u0304")
|
||||
case "v":
|
||||
return chromedp.KeyEvent("\u0301")
|
||||
case ">":
|
||||
return chromedp.KeyEvent("\u0303")
|
||||
case "Up":
|
||||
return chromedp.KeyEvent("\u0308")
|
||||
case "Dn":
|
||||
return chromedp.KeyEvent("\u0307")
|
||||
case "All": // Select all
|
||||
return chromedp.KeyEvent("a", chromedp.KeyModifiers(input.ModifierCtrl))
|
||||
}
|
||||
}
|
||||
// Keys
|
||||
if len(rq.keys) > 0 {
|
||||
log.Printf("%s Sending Keys: %#v\n", rq.r.RemoteAddr, rq.keys)
|
||||
return chromedp.KeyEvent(rq.keys)
|
||||
}
|
||||
// Navigate to URL
|
||||
log.Printf("%s Processing Navigate Request for %s\n", rq.r.RemoteAddr, rq.url)
|
||||
return chromedp.Navigate(rq.url)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
}
|
||||
ctx, cncl = chromedp.NewContext(actx)
|
||||
log.Printf("Created new context, try again")
|
||||
fmt.Fprintln(w, "Created new context, try again")
|
||||
}
|
||||
|
||||
func waitForRender() chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
timeout := *delay
|
||||
if timeout > 5*time.Second {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
ch := make(chan struct{}, 1)
|
||||
lctx, lcancel := context.WithCancel(ctx)
|
||||
defer lcancel()
|
||||
chromedp.ListenTarget(lctx, func(ev interface{}) {
|
||||
if e, ok := ev.(*page.EventLifecycleEvent); ok && e.Name == "networkAlmostIdle" {
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
select {
|
||||
case <-ch:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
var ready bool
|
||||
if err := chromedp.Evaluate(
|
||||
`document.readyState === "complete" && Array.from(document.images).every(i => i.complete)`,
|
||||
&ready,
|
||||
).Do(ctx); err != nil {
|
||||
return nil
|
||||
}
|
||||
if ready {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/chromedp/chromedp/issues/979
|
||||
func chromedpCaptureScreenshot(res *[]byte, h int64) chromedp.Action {
|
||||
if res == nil {
|
||||
panic("res cannot be nil") // TODO: do not panic here, return error
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// Capture Screenshot using CDP
|
||||
func (rq *wrpReq) captureScreenshot() {
|
||||
wrpCach.clear()
|
||||
var h int64
|
||||
var pngCap []byte
|
||||
chromedp.Run(ctx,
|
||||
emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), 10, rq.zoom, false),
|
||||
chromedp.Location(&rq.url),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
_, _, _, _, _, s, err := page.GetLayoutMetrics().Do(ctx)
|
||||
if err == nil {
|
||||
h = int64(math.Ceil(s.Height))
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
if rq.proxy {
|
||||
rq.url = strings.Replace(rq.url, "https://", "http://", 1)
|
||||
}
|
||||
log.Printf("%s Landed on: %s, Height: %v\n", rq.r.RemoteAddr, rq.url, h)
|
||||
height := int64(float64(rq.height) / rq.zoom)
|
||||
if rq.height == 0 && h > 0 {
|
||||
height = h + 30
|
||||
}
|
||||
chromedp.Run(
|
||||
ctx, emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), height, rq.zoom, false),
|
||||
waitForRender(),
|
||||
)
|
||||
// Capture screenshot...
|
||||
ctxErr(chromedp.Run(ctx, chromedpCaptureScreenshot(&pngCap, rq.height)), rq.w)
|
||||
seq := shortuuid.New()
|
||||
var imgExt string
|
||||
if rq.imgType == "gip" {
|
||||
imgExt = "gif"
|
||||
} else {
|
||||
imgExt = rq.imgType
|
||||
}
|
||||
imgPath := fmt.Sprintf("/img/%s.%s", seq, imgExt)
|
||||
mapPath := fmt.Sprintf("/map/%s.map", seq)
|
||||
wrpCach.addMap(mapPath, *rq)
|
||||
var sSize string
|
||||
var iW, iH int
|
||||
switch rq.imgType {
|
||||
case "gip":
|
||||
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 gipBuf bytes.Buffer
|
||||
err = gip.Encode(&gipBuf, i, nil)
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to encode GIP: %s\n", rq.r.RemoteAddr, err)
|
||||
fmt.Fprintf(rq.w, "<BR>Unable to encode GIP:<BR>%s<BR>\n", err)
|
||||
return
|
||||
}
|
||||
wrpCach.addImg(imgPath, gipBuf)
|
||||
sSize = fmt.Sprintf("%.0f KB", float32(len(gipBuf.Bytes()))/1024.0)
|
||||
iW = i.Bounds().Max.X
|
||||
iH = i.Bounds().Max.Y
|
||||
log.Printf("%s Encoded GIP image: %s, Size: %s, Res: %dx%d, Time: %vms\n", rq.r.RemoteAddr, imgPath, sSize, iW, iH, time.Since(st).Milliseconds())
|
||||
case "png":
|
||||
pngBuf := bytes.NewBuffer(pngCap)
|
||||
wrpCach.addImg(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)
|
||||
case "gif":
|
||||
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 gifBuf bytes.Buffer
|
||||
err = gif.Encode(&gifBuf, gifPalette(i, rq.nColors), &gif.Options{})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
wrpCach.addImg(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.nColors, 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: int(rq.jQual)})
|
||||
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
|
||||
}
|
||||
wrpCach.addImg(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, *defJpgQual, iW, iH, time.Since(st).Milliseconds())
|
||||
}
|
||||
if rq.proxy {
|
||||
rq.w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(rq.w, "<HTML><HEAD>%s<TITLE>%s</TITLE></HEAD><BODY BGCOLOR=\"%s\">"+
|
||||
"<A HREF=\"%s\"><IMG SRC=\"%s\" BORDER=\"0\" WIDTH=\"%d\" HEIGHT=\"%d\" ISMAP></A>"+
|
||||
"</BODY></HTML>", rq.baseTag(), rq.url, *bgColor, mapPath, imgPath, iW, iH)
|
||||
} else {
|
||||
rq.printUI(uiParams{
|
||||
bgColor: *bgColor,
|
||||
pageHeight: fmt.Sprintf("%d PX", h),
|
||||
imgSize: sSize,
|
||||
imgURL: imgPath,
|
||||
mapURL: mapPath,
|
||||
imgWidth: iW,
|
||||
imgHeight: iH,
|
||||
})
|
||||
}
|
||||
log.Printf("%s Done with capture for %s\n", rq.r.RemoteAddr, rq.url)
|
||||
}
|
||||
|
||||
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 := wrpCach.getMap(r.URL.Path)
|
||||
rq.r = r
|
||||
rq.w = w
|
||||
if !ok {
|
||||
fmt.Fprintf(w, "Unable to find map %s\n", r.URL.Path)
|
||||
log.Printf("Unable to find map %s\n", r.URL.Path)
|
||||
return
|
||||
}
|
||||
n, err := fmt.Sscanf(r.URL.RawQuery, "%d,%d", &rq.mouseX, &rq.mouseY)
|
||||
if err != nil || n != 2 {
|
||||
fmt.Fprintf(w, "n=%d, err=%s\n", n, err)
|
||||
log.Printf("%s ISMAP n=%d, err=%s\n", r.RemoteAddr, n, err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s WrpReq from ISMAP: %+v\n", r.RemoteAddr, rq)
|
||||
if len(rq.url) < 4 {
|
||||
rq.printUI(uiParams{})
|
||||
return
|
||||
}
|
||||
rq.navigate() // TODO: if error from navigate do not capture
|
||||
if rq.proxy {
|
||||
chromedp.Run(ctx, waitForRender())
|
||||
var loc string
|
||||
chromedp.Run(ctx, chromedp.Location(&loc))
|
||||
loc = strings.Replace(loc, "https://", "http://", 1)
|
||||
http.Redirect(w, r, loc, http.StatusFound)
|
||||
return
|
||||
}
|
||||
rq.captureScreenshot()
|
||||
}
|
||||
|
||||
// TODO: merge this with html mode IMGZ
|
||||
func imgServerMap(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s IMG Request for %s\n", r.RemoteAddr, r.URL.Path)
|
||||
imgBuf, ok := wrpCach.getImg(r.URL.Path)
|
||||
if !ok || imgBuf.Bytes() == nil {
|
||||
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
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, ".gif"):
|
||||
w.Header().Set("Content-Type", "image/gif")
|
||||
case strings.HasSuffix(r.URL.Path, ".png"):
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
case strings.HasSuffix(r.URL.Path, ".jpg"):
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(imgBuf.Bytes())))
|
||||
w.Header().Set("Cache-Control", "max-age=0")
|
||||
w.Header().Set("Expires", "-1")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Write(imgBuf.Bytes())
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
931
old/wrp.py
931
old/wrp.py
@@ -1,931 +0,0 @@
|
||||
#!/usr/bin/env python2.7
|
||||
|
||||
# wrp.py - Web Rendering Proxy - https://github.com/tenox7/wrp
|
||||
# A HTTP proxy service that renders the requested URL in to a image associated
|
||||
# with an imagemap of clickable links. This is an adaptation of previous works by
|
||||
# picidae.net and Paul Hammond.
|
||||
|
||||
__version__ = "2.0"
|
||||
|
||||
#
|
||||
# This program is based on the software picidae.py from picidae.net
|
||||
# It was modified by Antoni Sawicki and Natalia Portillo
|
||||
#
|
||||
# This program is based on the software webkit2png from Paul Hammond.
|
||||
# It was extended by picidae.net
|
||||
#
|
||||
# Copyright (c) 2013-2018 Antoni Sawicki
|
||||
# Copyright (c) 2012-2013 picidae.net
|
||||
# Copyright (c) 2004-2013 Paul Hammond
|
||||
# Copyright (c) 2017-2018 Natalia Portillo
|
||||
# Copyright (c) 2018 //gir.st/
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
# Configuration options:
|
||||
PORT = 8080
|
||||
WIDTH = 1024
|
||||
HEIGHT = 768
|
||||
ISMAP = False # ISMAP=True is Server side for Mosaic 1.1 and up. HTML 3.2 supports Client side maps (ISMAP=False)
|
||||
WAIT = 1 # sleep for 1 second to allow javascript renders
|
||||
QUALITY = 75 # For JPEG: image quality 0-100; For PNG: sets compression level (leftmost digit 0 fastest, 9 best)
|
||||
AUTOWIDTH = True # Check for browser width using javascript
|
||||
FORMAT = "AUTO" # AUTO = GIF for mac OS, JPG for rest; PNG, GIF, JPG as supported values.
|
||||
SSLSTRIP = True # enable to automatically downgrade secure requests
|
||||
|
||||
# PythonMagick configuration options
|
||||
MK_MONOCHROME = False # Convert the render to a black and white dithered image
|
||||
MK_GRAYSCALE = False # Convert the render to a grayscal dithered image
|
||||
MK_COLORS = 0 # Reduce number of colors in the image. 0 for not reducing. Less than 256 works in grayscale also.
|
||||
MK_DITHER = False # Dither the image to reduce size. GIFs will always be dithered. Ignored if MK_COLORS is not set.
|
||||
|
||||
import re
|
||||
import random
|
||||
import os
|
||||
import time
|
||||
import string
|
||||
import urllib
|
||||
import socket
|
||||
import SocketServer
|
||||
import SimpleHTTPServer
|
||||
import threading
|
||||
import Queue
|
||||
import sys
|
||||
import logging
|
||||
import StringIO
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
import PythonMagick
|
||||
HasMagick = True
|
||||
except ImportError:
|
||||
HasMagick = False
|
||||
|
||||
# Request queue (URLs go in here)
|
||||
REQ = Queue.Queue()
|
||||
# Response queue (dummy response objects)
|
||||
RESP = Queue.Queue()
|
||||
# Renders dictionary
|
||||
RENDERS = {}
|
||||
|
||||
#######################
|
||||
### Linux CODEPATH ###
|
||||
#######################
|
||||
|
||||
if sys.platform.startswith('linux') or sys.platform.startswith('freebsd'):
|
||||
try:
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWebKit import *
|
||||
from PyQt5.QtWebKitWidgets import *
|
||||
from PyQt5.QtNetwork import *
|
||||
from PyQt5.QtWidgets import *
|
||||
IsPyQt5 = True
|
||||
except ImportError:
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtWebKit import *
|
||||
from PyQt4.QtNetwork import *
|
||||
IsPyQt5 = False
|
||||
|
||||
# claunia: Check how to use this in macOS
|
||||
logging.basicConfig(filename='/dev/stdout', level=logging.WARN, )
|
||||
logger = logging.getLogger('wrp')
|
||||
|
||||
# Class for Website-Rendering. Uses QWebPage, which
|
||||
# requires a running QtGui to work.
|
||||
class WebkitRenderer(QObject):
|
||||
def __init__(self, **kwargs):
|
||||
"""Sets default values for the properties."""
|
||||
|
||||
if not QApplication.instance():
|
||||
raise RuntimeError(self.__class__.__name__ + \
|
||||
" requires a running QApplication instance")
|
||||
QObject.__init__(self)
|
||||
|
||||
# Initialize default properties
|
||||
self.width = kwargs.get('width', 0)
|
||||
self.height = kwargs.get('height', 0)
|
||||
self.timeout = kwargs.get('timeout', 0)
|
||||
self.wait = kwargs.get('wait', 0)
|
||||
self.logger = kwargs.get('logger', None)
|
||||
# Set this to true if you want to capture flash.
|
||||
# Not that your desktop must be large enough for
|
||||
# fitting the whole window.
|
||||
self.grabWholeWindow = kwargs.get('grabWholeWindow', False)
|
||||
|
||||
# Set some default options for QWebPage
|
||||
self.qWebSettings = {
|
||||
QWebSettings.JavascriptEnabled : True,
|
||||
QWebSettings.PluginsEnabled : True,
|
||||
QWebSettings.PrivateBrowsingEnabled : True,
|
||||
QWebSettings.JavascriptCanOpenWindows : False
|
||||
}
|
||||
|
||||
def render(self, url):
|
||||
"""Renders the given URL into a QImage object"""
|
||||
# We have to use this helper object because
|
||||
# QApplication.processEvents may be called, causing
|
||||
# this method to get called while it has not returned yet.
|
||||
helper = _WebkitRendererHelper(self)
|
||||
helper._window.resize(self.width, self.height)
|
||||
image = helper.render(url)
|
||||
|
||||
# Bind helper instance to this image to prevent the
|
||||
# object from being cleaned up (and with it the QWebPage, etc)
|
||||
# before the data has been used.
|
||||
image.helper = helper
|
||||
|
||||
return image
|
||||
|
||||
class _WebkitRendererHelper(QObject):
|
||||
"""This helper class is doing the real work. It is required to
|
||||
allow WebkitRenderer.render() to be called "asynchronously"
|
||||
(but always from Qt's GUI thread).
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""Copies the properties from the parent (WebkitRenderer) object,
|
||||
creates the required instances of QWebPage, QWebView and QMainWindow
|
||||
and registers some Slots.
|
||||
"""
|
||||
QObject.__init__(self)
|
||||
|
||||
# Copy properties from parent
|
||||
for key, value in parent.__dict__.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
# Create and connect required PyQt4 objects
|
||||
self._page = CustomWebPage(logger=self.logger)
|
||||
self._view = QWebView()
|
||||
self._view.setPage(self._page)
|
||||
self._window = QMainWindow()
|
||||
self._window.setCentralWidget(self._view)
|
||||
|
||||
# Import QWebSettings
|
||||
for key, value in self.qWebSettings.iteritems():
|
||||
self._page.settings().setAttribute(key, value)
|
||||
|
||||
# Connect required event listeners
|
||||
if IsPyQt5:
|
||||
self._page.loadFinished.connect(self._on_load_finished)
|
||||
self._page.loadStarted.connect(self._on_load_started)
|
||||
self._page.networkAccessManager().sslErrors.connect(self._on_ssl_errors)
|
||||
self._page.networkAccessManager().finished.connect(self._on_each_reply)
|
||||
else:
|
||||
self.connect(self._page, SIGNAL("loadFinished(bool)"), self._on_load_finished)
|
||||
self.connect(self._page, SIGNAL("loadStarted()"), self._on_load_started)
|
||||
self.connect(self._page.networkAccessManager(),
|
||||
SIGNAL("sslErrors(QNetworkReply *,const QList<QSslError>&)"),
|
||||
self._on_ssl_errors)
|
||||
self.connect(self._page.networkAccessManager(),
|
||||
SIGNAL("finished(QNetworkReply *)"),
|
||||
self._on_each_reply)
|
||||
|
||||
# The way we will use this, it seems to be unesseccary to have Scrollbars enabled
|
||||
self._page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||
self._page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
|
||||
self._page.settings().setUserStyleSheetUrl(
|
||||
QUrl("data:text/css,html,body{overflow-y:hidden !important;}"))
|
||||
|
||||
# Show this widget
|
||||
# self._window.show()
|
||||
|
||||
def __del__(self):
|
||||
"""Clean up Qt4 objects. """
|
||||
self._window.close()
|
||||
del self._window
|
||||
del self._view
|
||||
del self._page
|
||||
|
||||
def render(self, url):
|
||||
"""The real worker. Loads the page (_load_page) and awaits
|
||||
the end of the given 'delay'. While it is waiting outstanding
|
||||
QApplication events are processed.
|
||||
After the given delay, the Window or Widget (depends
|
||||
on the value of 'grabWholeWindow' is drawn into a QPixmap
|
||||
"""
|
||||
self._load_page(url, self.width, self.height, self.timeout)
|
||||
# Wait for end of timer. In this time, process
|
||||
# other outstanding Qt events.
|
||||
if self.wait > 0:
|
||||
if self.logger: self.logger.debug("Waiting %d seconds " % self.wait)
|
||||
waitToTime = time.time() + self.wait
|
||||
while time.time() < waitToTime:
|
||||
if QApplication.hasPendingEvents():
|
||||
QApplication.processEvents()
|
||||
|
||||
if self.grabWholeWindow:
|
||||
# Note that this does not fully ensure that the
|
||||
# window still has the focus when the screen is
|
||||
# grabbed. This might result in a race condition.
|
||||
self._view.activateWindow()
|
||||
if IsPyQt5:
|
||||
image = QScreen.grabWindow(self._window.winId())
|
||||
else:
|
||||
image = QPixmap.grabWindow(self._window.winId())
|
||||
else:
|
||||
if IsPyQt5:
|
||||
image = QWidget.grab(self._window)
|
||||
else:
|
||||
image = QPixmap.grabWidget(self._window)
|
||||
|
||||
httpout = WebkitRenderer.httpout
|
||||
|
||||
frame = self._view.page().currentFrame()
|
||||
web_url = frame.url().toString()
|
||||
|
||||
# Write URL map
|
||||
httpout.write("<!-- Web Rendering Proxy v%s by Antoni Sawicki -->\n"
|
||||
% (__version__))
|
||||
httpout.write("<!-- Request for [%s] frame [%s] -->\n"
|
||||
% (WebkitRenderer.req_url, web_url))
|
||||
# Get title
|
||||
httpout.write("<HTML><HEAD>")
|
||||
for ttl in frame.findAllElements('title'):
|
||||
httpout.write((u"<TITLE>%s</TITLE>"
|
||||
% ttl.toPlainText()).encode('utf-8', errors='ignore'))
|
||||
break # Don't repeat bad HTML coding with several title marks
|
||||
httpout.write("</HEAD>\n<BODY>\n")
|
||||
|
||||
if AUTOWIDTH:
|
||||
httpout.write("<script>document.write('<span style=\"display: none;\"><img src=\"http://width-' + document.body.clientWidth + '-px.jpg\" width=\"0\" height=\"0\"></span>');</script>\n")
|
||||
|
||||
if ISMAP == True:
|
||||
httpout.write("<A HREF=\"http://%s\">"
|
||||
"<IMG SRC=\"http://%s\" ALT=\"wrp-render\" ISMAP>\n"
|
||||
"</A>\n" % (WebkitRenderer.req_map, WebkitRenderer.req_img))
|
||||
mapfile = StringIO.StringIO()
|
||||
mapfile.write("default %s\n" % (web_url))
|
||||
else:
|
||||
httpout.write("<IMG SRC=\"http://%s\" ALT=\"wrp-render\" USEMAP=\"#map\">\n"
|
||||
"<MAP NAME=\"map\">\n" % (WebkitRenderer.req_img))
|
||||
|
||||
for x in frame.findAllElements('a'):
|
||||
turl = QUrl(web_url).resolved(QUrl(x.attribute('href'))).toString()
|
||||
xmin, ymin, xmax, ymax = x.geometry().getCoords()
|
||||
if ISMAP == True:
|
||||
mapfile.write("rect %s %i,%i %i,%i\n".decode('utf-8', errors='ignore') % (turl, xmin, ymin, xmax, ymax))
|
||||
else:
|
||||
httpout.write(("<AREA SHAPE=\"RECT\""
|
||||
" COORDS=\"%i,%i,%i,%i\""
|
||||
" ALT=\"%s\" HREF=\"%s\">\n".decode('utf-8', errors='ignore')
|
||||
% (xmin, ymin, xmax, ymax, turl, turl)).encode("utf-8"))
|
||||
|
||||
if ISMAP != True:
|
||||
httpout.write("</MAP>\n")
|
||||
|
||||
httpout.write("</BODY>\n</HTML>\n")
|
||||
|
||||
if ISMAP == True:
|
||||
RENDERS[WebkitRenderer.req_map] = mapfile
|
||||
|
||||
return image
|
||||
|
||||
def _load_page(self, url, width, height, timeout):
|
||||
"""
|
||||
This method implements the logic for retrieving and displaying
|
||||
the requested page.
|
||||
"""
|
||||
|
||||
# This is an event-based application. So we have to wait until
|
||||
# "loadFinished(bool)" raised.
|
||||
cancelAt = time.time() + timeout
|
||||
self.__loading = True
|
||||
self.__loadingResult = False # Default
|
||||
self._page.mainFrame().load(QUrl(url))
|
||||
while self.__loading:
|
||||
if timeout > 0 and time.time() >= cancelAt:
|
||||
raise RuntimeError("Request timed out on %s" % url)
|
||||
while QApplication.hasPendingEvents() and self.__loading:
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
if self.logger: self.logger.debug("Processing result")
|
||||
|
||||
if self.__loading_result == False:
|
||||
if self.logger: self.logger.warning("Failed to load %s" % url)
|
||||
|
||||
# Set initial viewport (the size of the "window")
|
||||
size = self._page.mainFrame().contentsSize()
|
||||
if self.logger: self.logger.debug("contentsSize: %s", size)
|
||||
if width > 0:
|
||||
size.setWidth(width)
|
||||
if height > 0:
|
||||
size.setHeight(height)
|
||||
|
||||
self._window.resize(size)
|
||||
|
||||
def _on_each_reply(self, reply):
|
||||
"""Logs each requested uri"""
|
||||
self.logger.debug("Received %s" % (reply.url().toString()))
|
||||
|
||||
# Eventhandler for "loadStarted()" signal
|
||||
def _on_load_started(self):
|
||||
"""Slot that sets the '__loading' property to true."""
|
||||
if self.logger: self.logger.debug("loading started")
|
||||
self.__loading = True
|
||||
|
||||
# Eventhandler for "loadFinished(bool)" signal
|
||||
def _on_load_finished(self, result):
|
||||
"""Slot that sets the '__loading' property to false and stores
|
||||
the result code in '__loading_result'.
|
||||
"""
|
||||
if self.logger: self.logger.debug("loading finished with result %s", result)
|
||||
self.__loading = False
|
||||
self.__loading_result = result
|
||||
|
||||
# Eventhandler for "sslErrors(QNetworkReply *,const QList<QSslError>&)" signal
|
||||
def _on_ssl_errors(self, reply, errors):
|
||||
"""Slot that writes SSL warnings into the log but ignores them."""
|
||||
for e in errors:
|
||||
if self.logger: self.logger.warn("SSL: " + e.errorString())
|
||||
reply.ignoreSslErrors()
|
||||
|
||||
class CustomWebPage(QWebPage):
|
||||
def __init__(self, **kwargs):
|
||||
super(CustomWebPage, self).__init__()
|
||||
self.logger = kwargs.get('logger', None)
|
||||
|
||||
def javaScriptAlert(self, frame, message):
|
||||
if self.logger: self.logger.debug('Alert: %s', message)
|
||||
|
||||
def javaScriptConfirm(self, frame, message):
|
||||
if self.logger: self.logger.debug('Confirm: %s', message)
|
||||
return False
|
||||
|
||||
def javaScriptPrompt(self, frame, message, default, result):
|
||||
"""This function is called whenever a JavaScript program running inside frame tries to
|
||||
prompt the user for input. The program may provide an optional message, msg, as well
|
||||
as a default value for the input in defaultValue.
|
||||
|
||||
If the prompt was cancelled by the user the implementation should return false;
|
||||
otherwise the result should be written to result and true should be returned.
|
||||
If the prompt was not cancelled by the user, the implementation should return true and
|
||||
the result string must not be null.
|
||||
"""
|
||||
if self.logger: self.logger.debug('Prompt: %s (%s)' % (message, default))
|
||||
return False
|
||||
|
||||
def shouldInterruptJavaScript(self):
|
||||
"""This function is called when a JavaScript program is running for a long period of
|
||||
time. If the user wanted to stop the JavaScript the implementation should return
|
||||
true; otherwise false.
|
||||
"""
|
||||
if self.logger: self.logger.debug("WebKit ask to interrupt JavaScript")
|
||||
return True
|
||||
|
||||
#===============================================================================
|
||||
|
||||
def init_qtgui(display=None, style=None, qtargs=None):
|
||||
"""Initiates the QApplication environment using the given args."""
|
||||
if QApplication.instance():
|
||||
logger.debug("QApplication has already been instantiated. \
|
||||
Ignoring given arguments and returning existing QApplication.")
|
||||
return QApplication.instance()
|
||||
|
||||
qtargs2 = [sys.argv[0]]
|
||||
|
||||
if display:
|
||||
qtargs2.append('-display')
|
||||
qtargs2.append(display)
|
||||
# Also export DISPLAY var as this may be used
|
||||
# by flash plugin
|
||||
os.environ["DISPLAY"] = display
|
||||
|
||||
if style:
|
||||
qtargs2.append('-style')
|
||||
qtargs2.append(style)
|
||||
|
||||
qtargs2.extend(qtargs or [])
|
||||
|
||||
return QApplication(qtargs2)
|
||||
|
||||
# Technically, this is a QtGui application, because QWebPage requires it
|
||||
# to be. But because we will have no user interaction, and rendering can
|
||||
# not start before 'app.exec_()' is called, we have to trigger our "main"
|
||||
# by a timer event.
|
||||
def __main_qt():
|
||||
# Render the page.
|
||||
# If this method times out or loading failed, a
|
||||
# RuntimeException is thrown
|
||||
try:
|
||||
while True:
|
||||
req = REQ.get()
|
||||
WebkitRenderer.httpout = req[0]
|
||||
WebkitRenderer.req_url = req[1]
|
||||
WebkitRenderer.req_img = req[2]
|
||||
WebkitRenderer.req_map = req[3]
|
||||
if WebkitRenderer.req_url == "http://wrp.stop/" or WebkitRenderer.req_url == "http://www.wrp.stop/":
|
||||
print ">>> Terminate Request Received"
|
||||
QApplication.exit(0)
|
||||
break
|
||||
|
||||
# Initialize WebkitRenderer object
|
||||
renderer = WebkitRenderer()
|
||||
renderer.logger = logger
|
||||
renderer.width = WIDTH
|
||||
renderer.height = HEIGHT
|
||||
renderer.timeout = 60
|
||||
renderer.wait = WAIT
|
||||
renderer.grabWholeWindow = False
|
||||
|
||||
image = renderer.render(WebkitRenderer.req_url)
|
||||
qBuffer = QBuffer()
|
||||
|
||||
if HasMagick:
|
||||
image.save(qBuffer, 'png', QUALITY)
|
||||
blob = PythonMagick.Blob(qBuffer.buffer().data())
|
||||
mimg = PythonMagick.Image(blob)
|
||||
mimg.quality(QUALITY)
|
||||
|
||||
if FORMAT=="GIF" and not MK_MONOCHROME and not MK_GRAYSCALE and not MK_DITHER and MK_COLORS != 0 and not MK_COLORS <= 256:
|
||||
mimg.quantizeColors(256)
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
|
||||
if MK_MONOCHROME:
|
||||
mimg.quantizeColorSpace(PythonMagick.ColorspaceType.GRAYColorspace)
|
||||
mimg.quantizeColors(2)
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
mimg.monochrome()
|
||||
elif MK_GRAYSCALE:
|
||||
mimg.quantizeColorSpace(PythonMagick.ColorspaceType.GRAYColorspace)
|
||||
if MK_COLORS > 0 and MK_COLORS < 256:
|
||||
mimg.quantizeColors(MK_COLORS)
|
||||
else:
|
||||
mimg.quantizeColors(256)
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
else:
|
||||
if MK_COLORS > 0:
|
||||
mimg.quantizeColors(MK_COLORS)
|
||||
if MK_DITHER:
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
|
||||
if FORMAT=="AUTO" or FORMAT=="JPG":
|
||||
mimg.write(blob, "jpg")
|
||||
elif FORMAT=="PNG":
|
||||
mimg.write(blob, "png")
|
||||
elif FORMAT=="GIF":
|
||||
mimg.write(blob, "gif")
|
||||
output = StringIO.StringIO()
|
||||
output.write(blob.data)
|
||||
else:
|
||||
if FORMAT=="AUTO" or FORMAT=="JPG":
|
||||
image.save(qBuffer, 'jpg', QUALITY)
|
||||
elif FORMAT=="PNG":
|
||||
image.save(qBuffer, 'png', QUALITY)
|
||||
|
||||
output = StringIO.StringIO()
|
||||
output.write(qBuffer.buffer().data())
|
||||
|
||||
RENDERS[req[2]] = output
|
||||
|
||||
del renderer
|
||||
print ">>> done: %s [%d kb]..." % (WebkitRenderer.req_img, output.len/1024)
|
||||
|
||||
RESP.put('')
|
||||
|
||||
QApplication.exit(0)
|
||||
except RuntimeError, e:
|
||||
logger.error("main: %s" % e)
|
||||
print >> sys.stderr, e
|
||||
QApplication.exit(1)
|
||||
|
||||
######################
|
||||
### macOS CODEPATH ###
|
||||
######################
|
||||
|
||||
elif sys.platform == "darwin":
|
||||
import Foundation
|
||||
import WebKit
|
||||
import AppKit
|
||||
import objc
|
||||
|
||||
class AppDelegate(Foundation.NSObject):
|
||||
# what happens when the app starts up
|
||||
def applicationDidFinishLaunching_(self, aNotification):
|
||||
webview = aNotification.object().windows()[0].contentView()
|
||||
webview.frameLoadDelegate().getURL(webview)
|
||||
|
||||
class WebkitLoad(Foundation.NSObject, WebKit.protocols.WebFrameLoadDelegate):
|
||||
# what happens if something goes wrong while loading
|
||||
def webView_didFailLoadWithError_forFrame_(self, webview, error, frame):
|
||||
if error.code() == Foundation.NSURLErrorCancelled:
|
||||
return
|
||||
print " ... something went wrong 1: " + error.localizedDescription()
|
||||
AppKit.NSApplication.sharedApplication().terminate_(None)
|
||||
|
||||
def webView_didFailProvisionalLoadWithError_forFrame_(self, webview, error, frame):
|
||||
if error.code() == Foundation.NSURLErrorCancelled:
|
||||
return
|
||||
print " ... something went wrong 2: " + error.localizedDescription()
|
||||
AppKit.NSApplication.sharedApplication().terminate_(None)
|
||||
|
||||
def getURL(self, webview):
|
||||
req = REQ.get()
|
||||
WebkitLoad.httpout = req[0]
|
||||
WebkitLoad.req_url = req[1]
|
||||
WebkitLoad.req_img = req[2]
|
||||
WebkitLoad.req_map = req[3]
|
||||
|
||||
if WebkitLoad.req_url == "http://wrp.stop/" or WebkitLoad.req_url == "http://www.wrp.stop/":
|
||||
print ">>> Terminate Request Received"
|
||||
AppKit.NSApplication.sharedApplication().terminate_(None)
|
||||
|
||||
nsurl = Foundation.NSURL.URLWithString_(WebkitLoad.req_url)
|
||||
if not (nsurl and nsurl.scheme()):
|
||||
nsurl = Foundation.NSURL.alloc().initFileURLWithPath_(WebkitLoad.req_url)
|
||||
nsurl = nsurl.absoluteURL()
|
||||
|
||||
Foundation.NSURLRequest.setAllowsAnyHTTPSCertificate_forHost_(objc.YES, nsurl.host())
|
||||
|
||||
self.resetWebview(webview)
|
||||
webview.mainFrame().loadRequest_(Foundation.NSURLRequest.requestWithURL_(nsurl))
|
||||
if not webview.mainFrame().provisionalDataSource():
|
||||
print " ... not a proper url?"
|
||||
RESP.put('')
|
||||
self.getURL(webview)
|
||||
|
||||
def resetWebview(self, webview):
|
||||
rect = Foundation.NSMakeRect(0, 0, WIDTH, HEIGHT)
|
||||
webview.window().setContentSize_((WIDTH, HEIGHT))
|
||||
webview.setFrame_(rect)
|
||||
|
||||
def captureView(self, view):
|
||||
view.window().display()
|
||||
view.window().setContentSize_(view.bounds().size)
|
||||
view.setFrame_(view.bounds())
|
||||
|
||||
if hasattr(view, "bitmapImageRepForCachingDisplayInRect_"):
|
||||
bitmapdata = view.bitmapImageRepForCachingDisplayInRect_(view.bounds())
|
||||
view.cacheDisplayInRect_toBitmapImageRep_(view.bounds(), bitmapdata)
|
||||
else:
|
||||
view.lockFocus()
|
||||
bitmapdata = AppKit.NSBitmapImageRep.alloc()
|
||||
bitmapdata.initWithFocusedViewRect_(view.bounds())
|
||||
view.unlockFocus()
|
||||
return bitmapdata
|
||||
|
||||
# what happens when the page has finished loading
|
||||
def webView_didFinishLoadForFrame_(self, webview, frame):
|
||||
# don't care about subframes
|
||||
if frame == webview.mainFrame():
|
||||
view = frame.frameView().documentView()
|
||||
|
||||
output = StringIO.StringIO()
|
||||
|
||||
if HasMagick:
|
||||
output.write(self.captureView(view).representationUsingType_properties_(
|
||||
AppKit.NSPNGFileType, None))
|
||||
blob = PythonMagick.Blob(output)
|
||||
mimg = PythonMagick.Image(blob)
|
||||
mimg.quality(QUALITY)
|
||||
|
||||
if FORMAT=="GIF" and not MK_MONOCHROME and not MK_GRAYSCALE and not MK_DITHER and MK_COLORS != 0 and not MK_COLORS <= 256:
|
||||
mimg.quantizeColors(256)
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
|
||||
if MK_MONOCHROME:
|
||||
mimg.quantizeColorSpace(PythonMagick.ColorspaceType.GRAYColorspace)
|
||||
mimg.quantizeColors(2)
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
mimg.monochrome()
|
||||
elif MK_GRAYSCALE:
|
||||
mimg.quantizeColorSpace(PythonMagick.ColorspaceType.GRAYColorspace)
|
||||
if MK_COLORS > 0 and MK_COLORS < 256:
|
||||
mimg.quantizeColors(MK_COLORS)
|
||||
else:
|
||||
mimg.quantizeColors(256)
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
else:
|
||||
if MK_COLORS > 0:
|
||||
mimg.quantizeColors(MK_COLORS)
|
||||
if MK_DITHER:
|
||||
mimg.quantizeDither()
|
||||
mimg.quantize()
|
||||
|
||||
if FORMAT=="JPG":
|
||||
mimg.write(blob, "jpg")
|
||||
elif FORMAT=="PNG":
|
||||
mimg.write(blob, "png")
|
||||
elif FORMAT=="AUTO" or FORMAT=="GIF":
|
||||
mimg.write(blob, "gif")
|
||||
output = StringIO.StringIO()
|
||||
output.write(blob.data)
|
||||
else:
|
||||
if FORMAT=="AUTO" or FORMAT=="GIF":
|
||||
output.write(self.captureView(view).representationUsingType_properties_(
|
||||
AppKit.NSGIFFileType, None))
|
||||
elif FORMAT=="JPG":
|
||||
output.write(self.captureView(view).representationUsingType_properties_(
|
||||
AppKit.NSJPEGFileType, None))
|
||||
elif FORMAT=="PNG":
|
||||
output.write(self.captureView(view).representationUsingType_properties_(
|
||||
AppKit.NSPNGFileType, None))
|
||||
|
||||
RENDERS[WebkitLoad.req_img] = output
|
||||
|
||||
# url of the rendered page
|
||||
web_url = frame.dataSource().initialRequest().URL().absoluteString()
|
||||
|
||||
httpout = WebkitLoad.httpout
|
||||
|
||||
httpout.write("<!-- Web Rendering Proxy v%s by Antoni Sawicki -->\n"
|
||||
% (__version__))
|
||||
httpout.write("<!-- Request for [%s] frame [%s] -->\n"
|
||||
% (WebkitLoad.req_url, web_url))
|
||||
|
||||
domdocument = frame.DOMDocument()
|
||||
# Get title
|
||||
httpout.write("<HTML><HEAD>")
|
||||
httpout.write((u"<TITLE>%s</TITLE>"
|
||||
% domdocument.title()).encode('utf-8', errors='ignore'))
|
||||
httpout.write("</HEAD>\n<BODY>\n")
|
||||
|
||||
if AUTOWIDTH:
|
||||
httpout.write("<script>document.write('<span style=\"display: none;\"><img src=\"http://width-' + document.body.clientWidth + '-px.jpg\" width=\"0\" height=\"0\"></span>');</script>\n")
|
||||
|
||||
if ISMAP == True:
|
||||
httpout.write("<A HREF=\"http://%s\">"
|
||||
"<IMG SRC=\"http://%s\" ALT=\"wrp-render\" ISMAP>\n"
|
||||
"</A>\n" % (WebkitLoad.req_map, WebkitLoad.req_img))
|
||||
mapfile = StringIO.StringIO()
|
||||
mapfile.write("default %s\n" % (web_url))
|
||||
else:
|
||||
httpout.write("<IMG SRC=\"http://%s\" ALT=\"wrp-render\" USEMAP=\"#map\">\n"
|
||||
"<MAP NAME=\"map\">\n" % (WebkitLoad.req_img))
|
||||
|
||||
domnodelist = domdocument.getElementsByTagName_('A')
|
||||
i = 0
|
||||
while i < domnodelist.length():
|
||||
turl = domnodelist.item_(i).valueForKey_('href')
|
||||
#TODO: crashes? validate url? insert web_url if wrong?
|
||||
myrect = domnodelist.item_(i).boundingBox()
|
||||
|
||||
xmin = Foundation.NSMinX(myrect)
|
||||
ymin = Foundation.NSMinY(myrect)
|
||||
xmax = Foundation.NSMaxX(myrect)
|
||||
ymax = Foundation.NSMaxY(myrect)
|
||||
|
||||
if ISMAP == True:
|
||||
mapfile.write("rect %s %i,%i %i,%i\n".decode('utf-8', errors='ignore') % (turl, xmin, ymin, xmax, ymax))
|
||||
else:
|
||||
httpout.write("<AREA SHAPE=\"RECT\""
|
||||
" COORDS=\"%i,%i,%i,%i\""
|
||||
" ALT=\"%s\" HREF=\"%s\">\n".decode('utf-8', errors='ignore')
|
||||
% (xmin, ymin, xmax, ymax, turl, turl))
|
||||
|
||||
i += 1
|
||||
|
||||
if ISMAP != True:
|
||||
httpout.write("</MAP>\n")
|
||||
|
||||
httpout.write("</BODY>\n</HTML>\n")
|
||||
|
||||
if ISMAP == True:
|
||||
RENDERS[WebkitLoad.req_map] = mapfile
|
||||
|
||||
# Return to Proxy thread and Loop...
|
||||
RESP.put('')
|
||||
self.getURL(webview)
|
||||
|
||||
def main_cocoa():
|
||||
# Launch NS Application
|
||||
AppKit.NSApplicationLoad()
|
||||
app = AppKit.NSApplication.sharedApplication()
|
||||
delegate = AppDelegate.alloc().init()
|
||||
AppKit.NSApp().setDelegate_(delegate)
|
||||
AppKit.NSBundle.mainBundle().infoDictionary()['NSAppTransportSecurity'] = \
|
||||
dict(NSAllowsArbitraryLoads=True)
|
||||
rect = Foundation.NSMakeRect(-16000, -16000, 100, 100)
|
||||
win = AppKit.NSWindow.alloc()
|
||||
win.initWithContentRect_styleMask_backing_defer_(rect, AppKit.NSBorderlessWindowMask, 2, 0)
|
||||
webview = WebKit.WebView.alloc()
|
||||
webview.initWithFrame_(rect)
|
||||
webview.mainFrame().frameView().setAllowsScrolling_(objc.NO)
|
||||
webkit_version = Foundation.NSBundle.bundleForClass_(WebKit.WebView). \
|
||||
objectForInfoDictionaryKey_(WebKit.kCFBundleVersionKey)[1:]
|
||||
webview.setApplicationNameForUserAgent_("Like-Version/6.0 Safari/%s wrp/%s"
|
||||
% (webkit_version, __version__))
|
||||
win.setContentView_(webview)
|
||||
loaddelegate = WebkitLoad.alloc().init()
|
||||
loaddelegate.options = [""]
|
||||
webview.setFrameLoadDelegate_(loaddelegate)
|
||||
app.run()
|
||||
|
||||
#######################
|
||||
### COMMON CODEPATH ###
|
||||
#######################
|
||||
class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
req_url = self.path
|
||||
httpout = self.wfile
|
||||
|
||||
map_re = re.match(r"http://(wrp-\d+\.map).*?(\d+),(\d+)", req_url)
|
||||
wid_re = re.match(r"http://(width-[0-9]+-px\.jpg).*", req_url)
|
||||
gif_re = re.match(r"http://(wrp-\d+\.gif).*", req_url)
|
||||
jpg_re = re.match(r"http://(wrp-\d+\.jpg).*", req_url)
|
||||
png_re = re.match(r"http://(wrp-\d+\.png).*", req_url)
|
||||
|
||||
# Serve Rendered GIF
|
||||
if gif_re:
|
||||
img = gif_re.group(1)
|
||||
print ">>> request for rendered gif image... %s [%d kb]" \
|
||||
% (img, RENDERS[img].len/1024)
|
||||
self.send_response(200, 'OK')
|
||||
self.send_header('Content-type', 'image/gif')
|
||||
self.end_headers()
|
||||
httpout.write(RENDERS[img].getvalue())
|
||||
del RENDERS[img]
|
||||
|
||||
elif jpg_re:
|
||||
img = jpg_re.group(1)
|
||||
print ">>> request for rendered jpg image... %s [%d kb]" \
|
||||
% (img, RENDERS[img].len/1024)
|
||||
self.send_response(200, 'OK')
|
||||
self.send_header('Content-type', 'image/jpeg')
|
||||
self.end_headers()
|
||||
httpout.write(RENDERS[img].getvalue())
|
||||
del RENDERS[img]
|
||||
|
||||
elif png_re:
|
||||
img = png_re.group(1)
|
||||
print ">>> request for rendered png image... %s [%d kb]" \
|
||||
% (img, RENDERS[img].len/1024)
|
||||
self.send_response(200, 'OK')
|
||||
self.send_header('Content-type', 'image/png')
|
||||
self.end_headers()
|
||||
httpout.write(RENDERS[img].getvalue())
|
||||
del RENDERS[img]
|
||||
|
||||
elif wid_re:
|
||||
global WIDTH
|
||||
try:
|
||||
wid = req_url.split("-")
|
||||
WIDTH = int(wid[1])
|
||||
print ">>> width request: %d" % WIDTH
|
||||
except:
|
||||
print ">>> width request error" % WIDTH
|
||||
|
||||
self.send_error(404, "Width request")
|
||||
self.end_headers()
|
||||
|
||||
# Process ISMAP Request
|
||||
elif map_re:
|
||||
map = map_re.group(1)
|
||||
req_x = int(map_re.group(2))
|
||||
req_y = int(map_re.group(3))
|
||||
print ">>> ISMAP request... %s [%d,%d] " % (map, req_x, req_y)
|
||||
|
||||
mapf = RENDERS[map]
|
||||
mapf.seek(0)
|
||||
goto_url = "none"
|
||||
for line in mapf.readlines():
|
||||
if re.match(r"(\S+)", line).group(1) == "default":
|
||||
default_url = re.match(r"\S+\s+(\S+)", line).group(1)
|
||||
|
||||
elif re.match(r"(\S+)", line).group(1) == "rect":
|
||||
try:
|
||||
rect = re.match(r"(\S+)\s+(\S+)\s+(\d+),(\d+)\s+(\d+),(\d+)", line)
|
||||
min_x = int(rect.group(3))
|
||||
min_y = int(rect.group(4))
|
||||
max_x = int(rect.group(5))
|
||||
max_y = int(rect.group(6))
|
||||
if (req_x >= min_x) and \
|
||||
(req_x <= max_x) and \
|
||||
(req_y >= min_y) and \
|
||||
(req_y <= max_y):
|
||||
goto_url = rect.group(2)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if goto_url == "none":
|
||||
goto_url = default_url
|
||||
|
||||
print ">>> ISMAP redirect: %s\n" % (goto_url)
|
||||
|
||||
self.send_response(302, "Found")
|
||||
self.send_header("Location", goto_url)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
httpout.write("<HTML><BODY><A HREF=\"%s\">%s</A></BODY></HTML>\n"
|
||||
% (goto_url, goto_url))
|
||||
|
||||
# Process a web page request and generate image
|
||||
else:
|
||||
print ">>> URL request... " + req_url
|
||||
|
||||
if req_url == "http://wrp.stop/" or req_url == "http://www.wrp.stop/":
|
||||
REQ.put((httpout, req_url, "", ""))
|
||||
RESP.get()
|
||||
else:
|
||||
reqst = urllib.urlopen(req_url)
|
||||
|
||||
if reqst.info().type == "text/html" or reqst.info().type == "application/xhtml+xml":
|
||||
# If an error occurs, send error headers to the requester
|
||||
if reqst.getcode() >= 400:
|
||||
self.send_response(reqst.getcode())
|
||||
for hdr in reqst.info():
|
||||
self.send_header(hdr, reqst.info()[hdr])
|
||||
self.end_headers()
|
||||
else:
|
||||
self.send_response(200, 'OK')
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
rnd = random.randrange(0, 1000)
|
||||
|
||||
if FORMAT == "GIF":
|
||||
req_extension = ".gif"
|
||||
elif FORMAT == "JPG":
|
||||
req_extension = ".jpg"
|
||||
elif FORMAT == "PNG":
|
||||
req_extension = ".png"
|
||||
elif (sys.platform.startswith('linux') or sys.platform.startswitch('freebsd')) and FORMAT == "AUTO":
|
||||
req_extension = ".jpg"
|
||||
elif sys.platform == "darwin" and FORMAT == "AUTO":
|
||||
req_extension = ".gif"
|
||||
|
||||
req_img = "wrp-%s%s" % (rnd, req_extension)
|
||||
req_map = "wrp-%s.map" % (rnd)
|
||||
|
||||
# To WebKit Thread
|
||||
REQ.put((httpout, req_url, req_img, req_map))
|
||||
# Wait for completition
|
||||
RESP.get()
|
||||
# If the requested file is not HTML or XHTML, just return it as is.
|
||||
else:
|
||||
self.send_response(reqst.getcode())
|
||||
for hdr in reqst.info():
|
||||
self.send_header(hdr, reqst.info()[hdr])
|
||||
self.end_headers()
|
||||
httpout.write(reqst.read())
|
||||
|
||||
def run_proxy():
|
||||
httpd = SocketServer.TCPServer(('', PORT), Proxy)
|
||||
print "Web Rendering Proxy v%s serving at port: %s" % (__version__, PORT)
|
||||
while 1:
|
||||
httpd.serve_forever()
|
||||
|
||||
def main():
|
||||
if(FORMAT != "AUTO" and FORMAT != "GIF" and FORMAT != "JPG" and FORMAT != "PNG"):
|
||||
sys.exit("Unsupported image format \"%s\". Exiting." % FORMAT)
|
||||
|
||||
if (sys.platform.startswith('linux') or sys.platform.startswith('freebsd')) and FORMAT == "GIF" and not HasMagick:
|
||||
sys.exit("GIF format is not supported on this platform. Exiting.")
|
||||
|
||||
# run traffic through sslstrip as a quick workaround for getting SSL webpages to work
|
||||
# NOTE: modern browsers are doing their best to stop this kind of 'attack'. Firefox
|
||||
# supports an about:config flag test.currentTimeOffsetSeconds(int) = 12000000, which
|
||||
# you can use to circumvent those checks.
|
||||
if SSLSTRIP:
|
||||
try:
|
||||
subprocess.check_output(["pidof", "sslstrip"])
|
||||
except:
|
||||
subprocess.Popen(["sslstrip"], stdout=open(os.devnull,'w'), stderr=subprocess.STDOUT) # runs on port 10000 by default
|
||||
QNetworkProxy.setApplicationProxy(QNetworkProxy(QNetworkProxy.HttpProxy, "localhost", 10000))
|
||||
# Launch Proxy Thread
|
||||
threading.Thread(target=run_proxy).start()
|
||||
|
||||
if sys.platform.startswith('linux') or sys.platform.startswith('freebsd'):
|
||||
import signal
|
||||
try:
|
||||
import PyQt5.QtCore
|
||||
except ImportError:
|
||||
import PyQt4.QtCore
|
||||
# Initialize Qt-Application, but make this script
|
||||
# abortable via CTRL-C
|
||||
app = init_qtgui(display=None, style=None)
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
QTimer.singleShot(0, __main_qt)
|
||||
sys.exit(app.exec_())
|
||||
elif sys.platform == "darwin":
|
||||
main_cocoa()
|
||||
else:
|
||||
sys.exit("Unsupported platform: %s. Exiting." % sys.platform)
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
398
shtml.go
Normal file
398
shtml.go
Normal file
@@ -0,0 +1,398 @@
|
||||
// WRP Simple HTML Mode Routines
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/srwiley/oksvg"
|
||||
"github.com/srwiley/rasterx"
|
||||
"github.com/tenox7/gip"
|
||||
"golang.org/x/image/webp"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var imgStor imageStore
|
||||
|
||||
const imgZpfx = "/imgz/"
|
||||
|
||||
func init() {
|
||||
imgStor.img = make(map[string]imageContainer)
|
||||
}
|
||||
|
||||
type imageContainer struct {
|
||||
data []byte
|
||||
url string
|
||||
added time.Time
|
||||
}
|
||||
|
||||
type imageStore struct {
|
||||
img map[string]imageContainer
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (i *imageStore) add(id, url string, img []byte) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
i.img[id] = imageContainer{data: img, url: url, added: time.Now()}
|
||||
}
|
||||
|
||||
func (i *imageStore) get(id string) ([]byte, error) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
img, ok := i.img[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
return img.data, nil
|
||||
}
|
||||
|
||||
func (i *imageStore) del(id string) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
delete(i.img, id)
|
||||
}
|
||||
|
||||
func (i *imageStore) clear() {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
i.img = make(map[string]imageContainer)
|
||||
}
|
||||
|
||||
func fetchImage(id, imgURL, imgType string, maxSize, imgOpt int) (int, int, int, error) {
|
||||
log.Printf("Downloading IMGZ URL=%q for ID=%q", imgURL, id)
|
||||
var in []byte
|
||||
var err error
|
||||
if len(imgURL) < 4 {
|
||||
return 0, 0, 0, fmt.Errorf("image URL too short: %q", imgURL)
|
||||
}
|
||||
switch imgURL[:4] {
|
||||
case "http":
|
||||
req, err := http.NewRequest("GET", imgURL, nil)
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("Error creating request for %q: %v", imgURL, err)
|
||||
}
|
||||
if *userAgent != "" {
|
||||
req.Header.Set("User-Agent", *userAgent)
|
||||
}
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("Error downloading %q: %v", imgURL, err)
|
||||
}
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return 0, 0, 0, fmt.Errorf("Error %q HTTP Status Code: %v", imgURL, r.StatusCode)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
in, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("Error reading %q: %v", imgURL, err)
|
||||
}
|
||||
case "data":
|
||||
idx := strings.Index(imgURL, ",")
|
||||
if idx < 1 {
|
||||
return 0, 0, 0, fmt.Errorf("image is embeded but unable to find coma: %q", imgURL)
|
||||
}
|
||||
in, err = base64.StdEncoding.DecodeString(imgURL[idx+1:])
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("error decoding image from url embed: %q: %v", imgURL, err)
|
||||
}
|
||||
default:
|
||||
return 0, 0, 0, fmt.Errorf("unsupported image URL scheme: %q", imgURL)
|
||||
}
|
||||
out, w, h, err := smallImg(in, imgType, maxSize, imgOpt)
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("Error scaling down %q: %v", imgURL, err)
|
||||
}
|
||||
imgStor.add(id, imgURL, out)
|
||||
return len(out), w, h, nil
|
||||
}
|
||||
|
||||
func decodeSVG(src []byte, maxSize int) (image.Image, error) {
|
||||
icon, err := oksvg.ReadIconStream(bytes.NewReader(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, h := int(icon.ViewBox.W), int(icon.ViewBox.H)
|
||||
if w <= 0 || h <= 0 {
|
||||
w, h = maxSize, maxSize
|
||||
}
|
||||
if w > maxSize || h > maxSize {
|
||||
ratio := float64(maxSize) / float64(max(w, h))
|
||||
w = int(float64(w) * ratio)
|
||||
h = int(float64(h) * ratio)
|
||||
}
|
||||
if w <= 0 {
|
||||
w = 1
|
||||
}
|
||||
if h <= 0 {
|
||||
h = 1
|
||||
}
|
||||
icon.SetTarget(0, 0, float64(w), float64(h))
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
icon.Draw(rasterx.NewDasher(w, h, rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())), 1)
|
||||
return rgba, nil
|
||||
}
|
||||
|
||||
func isSVG(src []byte) bool {
|
||||
s := strings.TrimSpace(string(src[:min(len(src), 256)]))
|
||||
return strings.HasPrefix(s, "<svg") || strings.HasPrefix(s, "<?xml")
|
||||
}
|
||||
|
||||
func smallImg(src []byte, imgType string, maxSize, imgOpt int) ([]byte, int, int, error) {
|
||||
t := http.DetectContentType(src)
|
||||
var err error
|
||||
var img image.Image
|
||||
switch {
|
||||
case t == "image/png":
|
||||
img, err = png.Decode(bytes.NewReader(src))
|
||||
case t == "image/gif":
|
||||
img, err = gif.Decode(bytes.NewReader(src))
|
||||
case t == "image/jpeg":
|
||||
img, err = jpeg.Decode(bytes.NewReader(src))
|
||||
case t == "image/webp":
|
||||
img, err = webp.Decode(bytes.NewReader(src))
|
||||
case t == "image/svg+xml", isSVG(src):
|
||||
img, err = decodeSVG(src, maxSize)
|
||||
default:
|
||||
err = errors.New("unknown content type: " + t)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("image decode problem: %v", err)
|
||||
}
|
||||
img = resize.Thumbnail(uint(maxSize), uint(maxSize), img, resize.NearestNeighbor)
|
||||
b := img.Bounds()
|
||||
var outBuf bytes.Buffer
|
||||
switch imgType {
|
||||
case "gip":
|
||||
err = gip.Encode(&outBuf, img, nil)
|
||||
case "png":
|
||||
err = png.Encode(&outBuf, img)
|
||||
case "gif":
|
||||
err = gif.Encode(&outBuf, gifPalette(img, int64(imgOpt)), &gif.Options{})
|
||||
case "jpg":
|
||||
err = jpeg.Encode(&outBuf, img, &jpeg.Options{Quality: imgOpt})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("image encode problem: %v", err)
|
||||
}
|
||||
return outBuf.Bytes(), b.Dx(), b.Dy(), nil
|
||||
}
|
||||
|
||||
var removeElements = []string{
|
||||
"script", "style", "link", "meta", "noscript", "iframe",
|
||||
"svg", "canvas", "video", "audio", "source", "picture",
|
||||
"template", "slot", "dialog", "portal",
|
||||
}
|
||||
|
||||
var renameToDiv = map[string]bool{
|
||||
"section": true, "article": true, "nav": true,
|
||||
"header": true, "footer": true, "aside": true,
|
||||
"main": true, "figure": true, "figcaption": true,
|
||||
"details": true, "summary": true, "hgroup": true,
|
||||
"mark": true, "time": true, "search": true,
|
||||
}
|
||||
|
||||
var keepAttrs = map[string]bool{
|
||||
"href": true, "src": true, "alt": true, "title": true,
|
||||
"width": true, "height": true, "border": true,
|
||||
"cellpadding": true, "cellspacing": true,
|
||||
"bgcolor": true, "background": true,
|
||||
"align": true, "valign": true,
|
||||
"colspan": true, "rowspan": true, "nowrap": true,
|
||||
"name": true, "value": true, "type": true,
|
||||
"action": true, "method": true, "enctype": true,
|
||||
"size": true, "maxlength": true,
|
||||
"checked": true, "selected": true, "multiple": true,
|
||||
"disabled": true, "readonly": true,
|
||||
"placeholder": true, "for": true,
|
||||
"rows": true, "cols": true,
|
||||
"color": true, "face": true,
|
||||
}
|
||||
|
||||
func resolveURL(raw string, base *url.URL) string {
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
ref, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return raw
|
||||
}
|
||||
return base.ResolveReference(ref).String()
|
||||
}
|
||||
|
||||
func simplifyDOM(doc *goquery.Document, rq *wrpReq) int {
|
||||
doc.Find(strings.Join(removeElements, ", ")).Remove()
|
||||
|
||||
doc.Find("*").Each(func(i int, s *goquery.Selection) {
|
||||
for _, n := range s.Nodes {
|
||||
if n.Type != html.ElementNode {
|
||||
return
|
||||
}
|
||||
if renameToDiv[n.Data] {
|
||||
n.Data = "div"
|
||||
}
|
||||
var keep []html.Attribute
|
||||
for _, a := range n.Attr {
|
||||
if keepAttrs[a.Key] {
|
||||
keep = append(keep, a)
|
||||
}
|
||||
}
|
||||
n.Attr = keep
|
||||
}
|
||||
})
|
||||
|
||||
imgExt := rq.imgType
|
||||
if imgExt == "gip" {
|
||||
imgExt = "gif"
|
||||
}
|
||||
var imgOpt int
|
||||
switch rq.imgType {
|
||||
case "jpg":
|
||||
imgOpt = int(rq.jQual)
|
||||
case "gif":
|
||||
imgOpt = int(rq.nColors)
|
||||
}
|
||||
baseURL, _ := url.Parse(rq.url)
|
||||
wrpParams := fmt.Sprintf("m=html&t=%s&s=%d", rq.imgType, rq.maxSize)
|
||||
|
||||
doc.Find("a[href]").Each(func(i int, s *goquery.Selection) {
|
||||
href, exists := s.Attr("href")
|
||||
if !exists || href == "" {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(href, "#") || strings.HasPrefix(href, "javascript:") {
|
||||
return
|
||||
}
|
||||
abs := resolveURL(href, baseURL)
|
||||
if rq.proxy {
|
||||
abs = strings.Replace(abs, "https://", "http://", 1)
|
||||
s.SetAttr("href", abs)
|
||||
} else {
|
||||
s.SetAttr("href", "/?"+wrpParams+"&url="+url.QueryEscape(abs))
|
||||
}
|
||||
})
|
||||
|
||||
type imgJob struct {
|
||||
sel *goquery.Selection
|
||||
seq string
|
||||
abs string
|
||||
}
|
||||
var jobs []imgJob
|
||||
doc.Find("img[src]").Each(func(i int, s *goquery.Selection) {
|
||||
src, exists := s.Attr("src")
|
||||
if !exists || src == "" {
|
||||
s.Remove()
|
||||
return
|
||||
}
|
||||
abs := resolveURL(src, baseURL)
|
||||
seq := shortuuid.New() + "." + imgExt
|
||||
jobs = append(jobs, imgJob{sel: s, seq: seq, abs: abs})
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var totSize int
|
||||
for i := range jobs {
|
||||
wg.Add(1)
|
||||
go func(j imgJob) {
|
||||
defer wg.Done()
|
||||
size, w, h, err := fetchImage(j.seq, j.abs, rq.imgType, int(rq.maxSize), imgOpt)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
j.sel.Remove()
|
||||
return
|
||||
}
|
||||
j.sel.SetAttr("src", imgZpfx+j.seq)
|
||||
j.sel.SetAttr("width", strconv.Itoa(w))
|
||||
j.sel.SetAttr("height", strconv.Itoa(h))
|
||||
totSize += size
|
||||
}(jobs[i])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return totSize
|
||||
}
|
||||
|
||||
func (rq *wrpReq) captureMarkdown() {
|
||||
imgStor.clear()
|
||||
log.Printf("Processing simple HTML conversion for %v", rq.url)
|
||||
var outerHTML string
|
||||
err := chromedp.Run(ctx,
|
||||
waitForRender(),
|
||||
emulation.SetEmulatedMedia().WithMedia("print"),
|
||||
chromedp.Evaluate(`(function(){document.querySelectorAll('*').forEach(function(e){if(getComputedStyle(e).display==='none')e.remove()})})()`, nil),
|
||||
chromedp.OuterHTML("html", &outerHTML, chromedp.ByQuery),
|
||||
emulation.SetEmulatedMedia().WithMedia(""),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get OuterHTML via CDP: %v", err)
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Got %v bytes HTML from CDP for %v", len(outerHTML), rq.url)
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(outerHTML))
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse HTML: %v", err)
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
totSize := simplifyDOM(doc, rq)
|
||||
|
||||
body := doc.Find("body")
|
||||
simplified, err := body.Html()
|
||||
if err != nil {
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Simplified to %v bytes html for %v", len(simplified), rq.url)
|
||||
|
||||
if rq.proxy {
|
||||
rq.w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(rq.w, "<HTML><HEAD>%s<TITLE>%s</TITLE></HEAD><BODY BGCOLOR=\"%s\">%s</BODY></HTML>",
|
||||
rq.baseTag(), rq.url, *bgColor, string(asciify([]byte(simplified))))
|
||||
return
|
||||
}
|
||||
rq.printUI(uiParams{
|
||||
text: string(asciify([]byte(simplified))),
|
||||
bgColor: *bgColor,
|
||||
imgSize: fmt.Sprintf("%.0f KB", float32(totSize)/1024.0),
|
||||
})
|
||||
}
|
||||
|
||||
func imgServerTxt(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s IMGZ Request for %s", r.RemoteAddr, r.URL.Path)
|
||||
id := strings.Replace(r.URL.Path, imgZpfx, "", 1)
|
||||
img, err := imgStor.get(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Printf("%s IMGZ error for %s: %v", r.RemoteAddr, r.URL.Path, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", http.DetectContentType(img))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(img)))
|
||||
w.Write(img)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
102
util.go
Normal file
102
util.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// wrp utility functions
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MaxHalford/halfgone"
|
||||
"github.com/ericpauley/go-quantize/quantize"
|
||||
)
|
||||
|
||||
func printMyIPs(b string) {
|
||||
ap := strings.Split(b, ":")
|
||||
if len(ap) < 2 {
|
||||
log.Fatal("Wrong format of ipaddress:port")
|
||||
}
|
||||
port := ap[len(ap)-1]
|
||||
if ap[0] != "" && ap[0] != "0.0.0.0" {
|
||||
log.Printf("Listen address: %v", b)
|
||||
return
|
||||
}
|
||||
a, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
log.Printf("Listen address: %v", b)
|
||||
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 += n.IP.String() + ":" + port + " "
|
||||
}
|
||||
log.Printf("Listen address: %v", m)
|
||||
}
|
||||
|
||||
func gifPalette(i image.Image, n int64) image.Image {
|
||||
switch n {
|
||||
case 2:
|
||||
i = halfgone.FloydSteinbergDitherer{}.Apply(halfgone.ImageToGray(i))
|
||||
default:
|
||||
q := quantize.MedianCutQuantizer{}
|
||||
p := q.Quantize(make([]color.Color, 0, int(n)), i)
|
||||
bounds := i.Bounds()
|
||||
quantized := image.NewPaletted(bounds, p)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
quantized.Set(x, y, i.At(x, y))
|
||||
}
|
||||
}
|
||||
i = quantized
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func fetchJnrbsnUserAgent() string {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get("https://jnrbsn.github.io/user-agents/user-agents.json")
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch user agents from jnrbsn: %v", err)
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Printf("jnrbsn API returned status: %d", resp.StatusCode)
|
||||
return ""
|
||||
}
|
||||
|
||||
var userAgents []string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userAgents); err != nil {
|
||||
log.Printf("Failed to decode jnrbsn user agents JSON: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(userAgents) == 0 {
|
||||
log.Printf("jnrbsn API returned no user agents")
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Printf("Fetched user agent from jnrbsn: %s", userAgents[0])
|
||||
return userAgents[0]
|
||||
}
|
||||
588
wrp.go
588
wrp.go
@@ -1,290 +1,404 @@
|
||||
//
|
||||
// WRP - Web Rendering Proxy
|
||||
//
|
||||
// Copyright (c) 2013-2018 Antoni Sawicki
|
||||
// Copyright (c) 2019 Google LLC
|
||||
// Copyright (c) 2013-2025 Antoni Sawicki
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/ericpauley/go-quantize/quantize"
|
||||
)
|
||||
|
||||
const version = "4.9.3"
|
||||
|
||||
var (
|
||||
addr = flag.String("l", ":8080", "Listen address:port, default :8080")
|
||||
headless = flag.Bool("h", true, "Headless mode / hide browser window (default true)")
|
||||
defType = flag.String("t", "gip", "Image type: gip|png|gif|jpg")
|
||||
wrpMode = flag.String("m", "ismap", "WRP Mode: ismap|html")
|
||||
defImgSize = flag.Int64("is", 200, "html mode default image size")
|
||||
defJpgQual = flag.Int64("q", 75, "Jpeg image quality, default 75%") // TODO: this should be form dropdown when jpeg is selected as image type
|
||||
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", 5*time.Second, "Timeout for waiting for the page to render before screenshot")
|
||||
userAgent = flag.String("ua", "jnrbsn", "override chrome user agent (jnrbsn=fetch from API, empty=default)")
|
||||
browserPath = flag.String("b", "", "browser executable path (e.g., /Applications/Brave Browser.app/Contents/MacOS/Brave Browser)")
|
||||
searchEng = flag.String("se", "https://duckduckgo.com/search?q=", "Search engine string")
|
||||
userDataDir = flag.String("profile", "", "Chrome user data dir for persistent cookies/sessions")
|
||||
bgColor = flag.String("bgcolor", "#F0F0F0", "Background color for WRP UI")
|
||||
)
|
||||
|
||||
var (
|
||||
version = "4.2"
|
||||
srv http.Server
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
gifmap = make(map[string]bytes.Buffer)
|
||||
ismap = make(map[string]wrpReq)
|
||||
srv http.Server
|
||||
actx, ctx context.Context
|
||||
acncl, cncl context.CancelFunc
|
||||
wrpCach wrpCache
|
||||
defGeom geom
|
||||
htmlTmpl *template.Template
|
||||
)
|
||||
|
||||
//go:embed *.html
|
||||
var fs embed.FS
|
||||
|
||||
type geom struct {
|
||||
w int64
|
||||
h int64
|
||||
c int64
|
||||
}
|
||||
|
||||
// TODO: there is a major overlap/duplication/triplication
|
||||
// between the 3 data structs, perhps we could reduce to just one?
|
||||
|
||||
// Data for html template
|
||||
type uiData struct {
|
||||
Version string
|
||||
WrpMode string
|
||||
URL string
|
||||
BgColor string
|
||||
NColors int64
|
||||
JQual int64
|
||||
Width int64
|
||||
Height int64
|
||||
Zoom float64
|
||||
ImgType string
|
||||
ImgURL string
|
||||
ImgSize string
|
||||
ImgWidth int
|
||||
ImgHeight int
|
||||
MaxSize int64
|
||||
MapURL string
|
||||
PageHeight string
|
||||
TeXT string
|
||||
}
|
||||
|
||||
// Parameters for HTML print function
|
||||
type uiParams struct {
|
||||
bgColor string
|
||||
pageHeight string
|
||||
imgSize string
|
||||
imgURL string
|
||||
mapURL string
|
||||
imgWidth int
|
||||
imgHeight int
|
||||
text string
|
||||
}
|
||||
|
||||
// WRP Request
|
||||
type wrpReq struct {
|
||||
U string // url
|
||||
W int64 // width
|
||||
H int64 // height
|
||||
S float64 // scale
|
||||
C int64 // #colors
|
||||
X int64 // mouseX
|
||||
Y int64 // mouseY
|
||||
K string // keys to send
|
||||
F string // Fn buttons
|
||||
url string
|
||||
width int64
|
||||
height int64
|
||||
zoom float64
|
||||
nColors int64
|
||||
jQual int64
|
||||
mouseX int64
|
||||
mouseY int64
|
||||
keys string
|
||||
buttons string
|
||||
imgType string
|
||||
wrpMode string
|
||||
maxSize int64
|
||||
proxy bool
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
}
|
||||
|
||||
func (w *wrpReq) parseForm(req *http.Request) {
|
||||
req.ParseForm()
|
||||
w.U = req.FormValue("url")
|
||||
if len(w.U) > 1 && !strings.HasPrefix(w.U, "http") {
|
||||
w.U = fmt.Sprintf("http://www.google.com/search?q=%s", url.QueryEscape(w.U))
|
||||
func (rq *wrpReq) baseTag() string {
|
||||
if rq.r.Method != "CONNECT" {
|
||||
return ""
|
||||
}
|
||||
w.W, _ = strconv.ParseInt(req.FormValue("w"), 10, 64)
|
||||
if w.W < 10 {
|
||||
w.W = 1152
|
||||
if addr := rq.r.Context().Value(http.LocalAddrContextKey); addr != nil {
|
||||
return fmt.Sprintf(`<BASE HREF="http://%v/">`, addr)
|
||||
}
|
||||
w.H, _ = strconv.ParseInt(req.FormValue("h"), 10, 64)
|
||||
if w.H < 10 {
|
||||
w.H = 600
|
||||
}
|
||||
w.S, _ = strconv.ParseFloat(req.FormValue("s"), 64)
|
||||
if w.S < 0.1 {
|
||||
w.S = 1.0
|
||||
}
|
||||
w.C, _ = strconv.ParseInt(req.FormValue("c"), 10, 64)
|
||||
if w.C < 2 || w.C > 256 {
|
||||
w.C = 256
|
||||
}
|
||||
w.K = req.FormValue("k")
|
||||
w.F = req.FormValue("Fn")
|
||||
log.Printf("WrpReq from Form: %+v\n", w)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w wrpReq) printPage(out http.ResponseWriter) {
|
||||
out.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(out, "<!-- Web Rendering Proxy Version %s -->\n", version)
|
||||
fmt.Fprintf(out, "<HTML>\n<HEAD><TITLE>WRP %s</TITLE></HEAD>\n<BODY BGCOLOR=\"#F0F0F0\">\n", w.U)
|
||||
fmt.Fprintf(out, "<FORM ACTION=\"/\" METHOD=\"POST\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"TEXT\" NAME=\"url\" VALUE=\"%s\" SIZE=\"10\">", w.U)
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" VALUE=\"Go\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\"Bk\">\n")
|
||||
fmt.Fprintf(out, "W <INPUT TYPE=\"TEXT\" NAME=\"w\" VALUE=\"%d\" SIZE=\"4\"> \n", w.W)
|
||||
fmt.Fprintf(out, "H <INPUT TYPE=\"TEXT\" NAME=\"h\" VALUE=\"%d\" SIZE=\"4\"> \n", w.H)
|
||||
fmt.Fprintf(out, "S <INPUT TYPE=\"TEXT\" NAME=\"s\" VALUE=\"%1.2f\" SIZE=\"3\"> \n", w.S)
|
||||
fmt.Fprintf(out, "C <INPUT TYPE=\"TEXT\" NAME=\"c\" VALUE=\"%d\" SIZE=\"3\">\n", w.C)
|
||||
fmt.Fprintf(out, "K <INPUT TYPE=\"TEXT\" NAME=\"k\" VALUE=\"\" SIZE=\"4\"> \n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\"Bs\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\"Rt\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\"<\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\"^\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\"v\">\n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"Fn\" VALUE=\">\" SIZE=\"1\">\n")
|
||||
fmt.Fprintf(out, "</FORM><BR>\n")
|
||||
func (rq *wrpReq) parseForm() {
|
||||
rq.r.ParseForm()
|
||||
rq.wrpMode = rq.r.FormValue("m")
|
||||
if rq.wrpMode == "" {
|
||||
rq.wrpMode = *wrpMode
|
||||
}
|
||||
rq.url = rq.r.FormValue("url")
|
||||
if len(rq.url) > 1 && !strings.HasPrefix(rq.url, "http") {
|
||||
rq.url = *searchEng + url.QueryEscape(rq.url)
|
||||
}
|
||||
// TODO: implement atoiOrZero
|
||||
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
|
||||
}
|
||||
rq.zoom, _ = strconv.ParseFloat(rq.r.FormValue("z"), 64)
|
||||
if rq.zoom < 0.1 {
|
||||
rq.zoom = 1.0
|
||||
}
|
||||
rq.imgType = rq.r.FormValue("t")
|
||||
switch rq.imgType {
|
||||
case "gip", "png", "gif", "jpg":
|
||||
default:
|
||||
rq.imgType = *defType
|
||||
}
|
||||
rq.nColors, _ = strconv.ParseInt(rq.r.FormValue("c"), 10, 64)
|
||||
if rq.nColors < 2 || rq.nColors > 256 {
|
||||
rq.nColors = defGeom.c
|
||||
}
|
||||
rq.jQual, _ = strconv.ParseInt(rq.r.FormValue("q"), 10, 64)
|
||||
if rq.jQual < 1 || rq.jQual > 100 {
|
||||
rq.jQual = *defJpgQual
|
||||
}
|
||||
rq.keys = rq.r.FormValue("k")
|
||||
rq.buttons = rq.r.FormValue("Fn")
|
||||
rq.maxSize, _ = strconv.ParseInt(rq.r.FormValue("s"), 10, 64)
|
||||
if rq.maxSize == 0 {
|
||||
rq.maxSize = *defImgSize
|
||||
}
|
||||
log.Printf("%s WrpReq from UI Form: %+v\n", rq.r.RemoteAddr, rq)
|
||||
}
|
||||
|
||||
func (w wrpReq) printFooter(out http.ResponseWriter) {
|
||||
fmt.Fprintf(out, "\n<P><A HREF=\"/?url=https://github.com/tenox7/wrp/&w=%d&h=%d&s=%1.2f&c=%d\">"+
|
||||
"Web Rendering Proxy Version %s</A> | <A HREF=\"/shutdown/\">Shutdown WRP</A></BODY>\n</HTML>\n", w.W, w.H, w.S, w.C, version)
|
||||
}
|
||||
|
||||
func pageServer(out http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s Page Request for %s [%+v]\n", req.RemoteAddr, req.URL.Path, req.URL.RawQuery)
|
||||
var w wrpReq
|
||||
w.parseForm(req)
|
||||
|
||||
if len(w.U) > 4 {
|
||||
w.capture(req.RemoteAddr, out)
|
||||
} else {
|
||||
w.printPage(out)
|
||||
w.printFooter(out)
|
||||
func (rq *wrpReq) printUI(p uiParams) {
|
||||
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")
|
||||
if p.bgColor == "" {
|
||||
p.bgColor = *bgColor
|
||||
}
|
||||
}
|
||||
|
||||
func mapServer(out http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s ISMAP Request for %s [%+v]\n", req.RemoteAddr, req.URL.Path, req.URL.RawQuery)
|
||||
w, ok := ismap[req.URL.Path]
|
||||
if !ok {
|
||||
fmt.Fprintf(out, "Unable to find map %s\n", req.URL.Path)
|
||||
log.Printf("Unable to find map %s\n", req.URL.Path)
|
||||
return
|
||||
data := uiData{
|
||||
Version: version,
|
||||
WrpMode: rq.wrpMode,
|
||||
URL: rq.url,
|
||||
BgColor: p.bgColor,
|
||||
Width: rq.width,
|
||||
Height: rq.height,
|
||||
NColors: rq.nColors,
|
||||
JQual: rq.jQual,
|
||||
Zoom: rq.zoom,
|
||||
MaxSize: rq.maxSize,
|
||||
ImgType: rq.imgType,
|
||||
ImgSize: p.imgSize,
|
||||
ImgWidth: p.imgWidth,
|
||||
ImgHeight: p.imgHeight,
|
||||
ImgURL: p.imgURL,
|
||||
MapURL: p.mapURL,
|
||||
PageHeight: p.pageHeight,
|
||||
TeXT: p.text,
|
||||
}
|
||||
defer delete(ismap, req.URL.Path)
|
||||
n, err := fmt.Sscanf(req.URL.RawQuery, "%d,%d", &w.X, &w.Y)
|
||||
if err != nil || n != 2 {
|
||||
fmt.Fprintf(out, "n=%d, err=%s\n", n, err)
|
||||
log.Printf("%s ISMAP n=%d, err=%s\n", req.RemoteAddr, n, err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s WrpReq from ISMAP: %+v\n", req.RemoteAddr, w)
|
||||
if len(w.U) > 4 {
|
||||
w.capture(req.RemoteAddr, out)
|
||||
} else {
|
||||
w.printPage(out)
|
||||
w.printFooter(out)
|
||||
}
|
||||
}
|
||||
|
||||
func imgServer(out http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s IMG Request for %s\n", req.RemoteAddr, req.URL.Path)
|
||||
gifbuf, ok := gifmap[req.URL.Path]
|
||||
if !ok || gifbuf.Bytes() == nil {
|
||||
fmt.Fprintf(out, "Unable to find image %s\n", req.URL.Path)
|
||||
log.Printf("Unable to find image %s\n", req.URL.Path)
|
||||
return
|
||||
}
|
||||
defer delete(gifmap, req.URL.Path)
|
||||
out.Header().Set("Content-Type", "image/gif")
|
||||
out.Header().Set("Content-Length", strconv.Itoa(len(gifbuf.Bytes())))
|
||||
out.Write(gifbuf.Bytes())
|
||||
out.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func (w wrpReq) capture(c string, out http.ResponseWriter) {
|
||||
var pngbuf []byte
|
||||
var gifbuf bytes.Buffer
|
||||
var err error
|
||||
|
||||
if w.X > 0 && w.Y > 0 {
|
||||
log.Printf("%s Mouse Click %d,%d\n", c, w.X, w.Y)
|
||||
err = chromedp.Run(ctx, chromedp.MouseClickXY(int64(float64(w.X)/w.S), int64(float64(w.Y)/w.S)))
|
||||
} else if len(w.F) > 0 {
|
||||
log.Printf("%s Button %v\n", c, w.F)
|
||||
switch w.F {
|
||||
case "Bk":
|
||||
err = chromedp.Run(ctx, chromedp.NavigateBack())
|
||||
case "Bs":
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent("\b"))
|
||||
case "Rt":
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent("\r"))
|
||||
case "<":
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent("\u0302"))
|
||||
case "^":
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent("\u0304"))
|
||||
case "v":
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent("\u0301"))
|
||||
case ">":
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent("\u0303"))
|
||||
}
|
||||
} else if len(w.K) > 0 {
|
||||
log.Printf("%s Sending Keys: %#v\n", c, w.K)
|
||||
err = chromedp.Run(ctx, chromedp.KeyEvent(w.K))
|
||||
} else {
|
||||
log.Printf("%s Processing Capture Request for %s\n", c, w.U)
|
||||
err = chromedp.Run(ctx,
|
||||
emulation.SetDeviceMetricsOverride(int64(float64(w.W)/w.S), int64(float64(w.H)/w.S), w.S, false),
|
||||
chromedp.Navigate(w.U),
|
||||
)
|
||||
}
|
||||
|
||||
err := htmlTmpl.Execute(rq.w, data)
|
||||
if err != nil {
|
||||
if err.Error() == "context canceled" {
|
||||
log.Printf("%s Contex cancelled, try again", c)
|
||||
fmt.Fprintf(out, "<BR>%s<BR> -- restarting, try again", err)
|
||||
ctx, cancel = chromedp.NewContext(context.Background())
|
||||
} else {
|
||||
log.Printf("%s %s", c, err)
|
||||
fmt.Fprintf(out, "<BR>%s<BR>", err)
|
||||
}
|
||||
return
|
||||
fmt.Fprintf(rq.w, "Error: %v", err)
|
||||
}
|
||||
|
||||
chromedp.Run(
|
||||
ctx, chromedp.Sleep(time.Second*3),
|
||||
chromedp.Location(&w.U),
|
||||
)
|
||||
log.Printf("%s Landed on: %s\n", c, w.U)
|
||||
w.printPage(out)
|
||||
|
||||
// Process Screenshot Image
|
||||
err = chromedp.Run(ctx, chromedp.CaptureScreenshot(&pngbuf))
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to capture screenshot: %s\n", c, err)
|
||||
fmt.Fprintf(out, "<BR>Unable to capture screenshot:<BR>%s<BR>\n", err)
|
||||
return
|
||||
}
|
||||
bytes.NewReader(pngbuf).Seek(0, 0)
|
||||
img, err := png.Decode(bytes.NewReader(pngbuf))
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to decode screenshot: %s\n", c, err)
|
||||
fmt.Fprintf(out, "<BR>Unable to decode page screenshot:<BR>%s<BR>\n", err)
|
||||
return
|
||||
}
|
||||
gifbuf.Reset()
|
||||
err = gif.Encode(&gifbuf, img, &gif.Options{NumColors: int(w.C), Quantizer: quantize.MedianCutQuantizer{}})
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to encode GIF: %s\n", c, err)
|
||||
fmt.Fprintf(out, "<BR>Unable to encode GIF:<BR>%s<BR>\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Compose map and gif
|
||||
seq := rand.Intn(9999)
|
||||
imgpath := fmt.Sprintf("/img/%04d.gif", seq)
|
||||
mappath := fmt.Sprintf("/map/%04d.map", seq)
|
||||
gifmap[imgpath] = gifbuf
|
||||
ismap[mappath] = w
|
||||
log.Printf("%s Encoded GIF image: %s, Size: %dKB, Colors: %d\n", c, imgpath, len(gifbuf.Bytes())/1024, w.C)
|
||||
fmt.Fprintf(out, "<A HREF=\"%s\"><IMG SRC=\"%s\" ALT=\"wrp\" BORDER=\"0\" ISMAP></A>", mappath, imgpath)
|
||||
w.printFooter(out)
|
||||
log.Printf("%s Done with caputure for %s\n", c, w.U)
|
||||
}
|
||||
|
||||
func haltServer(out http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s Shutdown Request for %s\n", req.RemoteAddr, req.URL.Path)
|
||||
defer cancel()
|
||||
func proxyServer(w http.ResponseWriter, r *http.Request) {
|
||||
var purl string
|
||||
switch {
|
||||
case r.Method == "CONNECT":
|
||||
purl = "https://" + r.Host
|
||||
case r.URL.Scheme == "":
|
||||
purl = "http://" + r.Host + r.URL.RequestURI()
|
||||
default:
|
||||
purl = r.URL.String()
|
||||
}
|
||||
log.Printf("%s Proxy Request for %s\n", r.RemoteAddr, purl)
|
||||
rq := wrpReq{
|
||||
r: r,
|
||||
w: w,
|
||||
url: purl,
|
||||
width: defGeom.w,
|
||||
height: defGeom.h,
|
||||
nColors: defGeom.c,
|
||||
zoom: 1.0,
|
||||
imgType: *defType,
|
||||
wrpMode: *wrpMode,
|
||||
maxSize: *defImgSize,
|
||||
jQual: *defJpgQual,
|
||||
proxy: true,
|
||||
}
|
||||
var currentURL string
|
||||
chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||||
currentURL = strings.Replace(currentURL, "https://", "http://", 1)
|
||||
if currentURL != strings.Replace(rq.url, "https://", "http://", 1) {
|
||||
rq.navigate()
|
||||
}
|
||||
rq.url = strings.Replace(rq.url, "https://", "http://", 1)
|
||||
if r.Method == "CONNECT" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
if rq.wrpMode == "html" {
|
||||
rq.captureMarkdown()
|
||||
return
|
||||
}
|
||||
rq.captureScreenshot()
|
||||
}
|
||||
|
||||
func isProxyRequest(r *http.Request) bool {
|
||||
return r.URL.IsAbs() || r.Method == "CONNECT"
|
||||
}
|
||||
|
||||
func pageServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s %s [%+v]\n", r.RemoteAddr, r.Method, r.URL, r.Host)
|
||||
if isProxyRequest(r) {
|
||||
proxyServer(w, r)
|
||||
return
|
||||
}
|
||||
log.Printf("%s Page Request for %s [%+v]\n", r.RemoteAddr, r.URL.Path, r.URL.RawQuery)
|
||||
rq := wrpReq{
|
||||
r: r,
|
||||
w: w,
|
||||
}
|
||||
rq.parseForm()
|
||||
if len(rq.url) < 4 {
|
||||
rq.printUI(uiParams{})
|
||||
return
|
||||
}
|
||||
rq.navigate() // TODO: if error from navigate do not capture
|
||||
if rq.wrpMode == "html" {
|
||||
rq.captureMarkdown()
|
||||
return
|
||||
}
|
||||
rq.captureScreenshot()
|
||||
}
|
||||
|
||||
func pacServer(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-ns-proxy-autoconfig")
|
||||
fmt.Fprintf(w, `function FindProxyForURL(url, host) {
|
||||
if (isPlainHostName(host) ||
|
||||
host == "localhost" ||
|
||||
isInNet(host, "127.0.0.0", "255.0.0.0") ||
|
||||
isInNet(host, "10.0.0.0", "255.0.0.0") ||
|
||||
isInNet(host, "172.16.0.0", "255.240.0.0") ||
|
||||
isInNet(host, "192.168.0.0", "255.255.0.0") ||
|
||||
isInNet(host, "169.254.0.0", "255.255.0.0"))
|
||||
return "DIRECT";
|
||||
return "PROXY %s";
|
||||
}
|
||||
`, r.Host)
|
||||
}
|
||||
|
||||
func haltServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s Shutdown Request for %s\n", r.RemoteAddr, r.URL.Path)
|
||||
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)
|
||||
}
|
||||
|
||||
func wrpTemplate(t string) string {
|
||||
var tmpl []byte
|
||||
fh, err := os.Open(t)
|
||||
if err != nil {
|
||||
goto builtin
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
tmpl, err = io.ReadAll(fh)
|
||||
if err != nil {
|
||||
goto builtin
|
||||
}
|
||||
log.Printf("Got HTML UI template from %v file, size %v \n", t, len(tmpl))
|
||||
return string(tmpl)
|
||||
|
||||
builtin:
|
||||
fhs, err := fs.Open("wrp.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer fhs.Close()
|
||||
|
||||
tmpl, err = io.ReadAll(fhs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Got HTML UI template from embed\n")
|
||||
return string(tmpl)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var addr string
|
||||
var head, headless bool
|
||||
var debug bool
|
||||
flag.StringVar(&addr, "l", ":8080", "Listen address:port, default :8080")
|
||||
flag.BoolVar(&head, "h", false, "Headed mode - display browser window")
|
||||
flag.BoolVar(&debug, "d", false, "Debug ChromeDP")
|
||||
var err error
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
flag.Parse()
|
||||
if head {
|
||||
headless = false
|
||||
} else {
|
||||
headless = true
|
||||
log.Printf("Web Rendering Proxy Version %s (%v)\n", version, runtime.GOARCH)
|
||||
log.Printf("Using embedded ca-certs from github.com/breml/rootcerts")
|
||||
log.Printf("Args: %q", os.Args)
|
||||
if len(os.Getenv("PORT")) > 0 {
|
||||
*addr = ":" + os.Getenv(("PORT"))
|
||||
}
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("hide-scrollbars", false),
|
||||
)
|
||||
actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer cancel()
|
||||
if debug {
|
||||
ctx, cancel = chromedp.NewContext(actx, chromedp.WithDebugf(log.Printf))
|
||||
} else {
|
||||
ctx, cancel = chromedp.NewContext(actx)
|
||||
printMyIPs(*addr)
|
||||
log.Printf("Default mode: %v", *wrpMode)
|
||||
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)
|
||||
}
|
||||
defer cancel()
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
cncl, acncl = chromedpStart()
|
||||
defer cncl()
|
||||
defer acncl()
|
||||
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
log.Printf("Interrupt - shutting down.")
|
||||
cncl()
|
||||
acncl()
|
||||
srv.Shutdown(context.Background())
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
wrpCach.imgs = make(map[string]cachedImg)
|
||||
wrpCach.maps = make(map[string]cachedMap)
|
||||
|
||||
http.HandleFunc("/", pageServer)
|
||||
http.HandleFunc("/map/", mapServer)
|
||||
http.HandleFunc("/img/", imgServer)
|
||||
http.HandleFunc("/img/", imgServerMap)
|
||||
http.HandleFunc(imgZpfx, imgServerTxt)
|
||||
http.HandleFunc("/proxy.pac", pacServer)
|
||||
http.HandleFunc("/shutdown/", haltServer)
|
||||
http.HandleFunc("/favicon.ico", http.NotFound)
|
||||
log.Printf("Web Rendering Proxy Version %s\n", version)
|
||||
log.Printf("Starting WRP http server on %s\n", addr)
|
||||
srv.Addr = addr
|
||||
err := srv.ListenAndServe()
|
||||
|
||||
log.Printf("Default Img Type: %v, Geometry: %+v", *defType, defGeom)
|
||||
|
||||
htmlTmpl, err = template.New("wrp.html").Parse(wrpTemplate(*htmFnam))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Print("Starting WRP http server")
|
||||
srv.Addr = *addr
|
||||
srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
pageServer(w, r)
|
||||
return
|
||||
}
|
||||
http.DefaultServeMux.ServeHTTP(w, r)
|
||||
})
|
||||
err = srv.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
87
wrp.html
Normal file
87
wrp.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>WRP {{.URL}}</TITLE>
|
||||
</HEAD>
|
||||
<BODY BGCOLOR="{{.BgColor}}">
|
||||
<FORM ACTION="/" METHOD="POST">
|
||||
<INPUT TYPE="TEXT" NAME="url" VALUE="{{.URL}}" SIZE="20">
|
||||
<INPUT TYPE="SUBMIT" VALUE="Go">
|
||||
{{ if eq .WrpMode "ismap" }}
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="Bk">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="St">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="Re">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="Up">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="Dn">
|
||||
W <INPUT TYPE="TEXT" NAME="w" VALUE="{{.Width}}" SIZE="4">
|
||||
H <INPUT TYPE="TEXT" NAME="h" VALUE="{{.Height}}" SIZE="4">
|
||||
{{ end }}
|
||||
{{ if eq .WrpMode "html" }}
|
||||
S <INPUT TYPE="TEXT" NAME="s" VALUE="{{.MaxSize}}" SIZE="4">
|
||||
{{ end }}
|
||||
{{ if eq .WrpMode "ismap" }}
|
||||
Z <SELECT NAME="z">
|
||||
<OPTION DISABLED>Zoom</OPTION>
|
||||
<OPTION VALUE="0.7" {{ if eq .Zoom 0.7}}SELECTED{{end}}>0.7 x</OPTION>
|
||||
<OPTION VALUE="0.8" {{ if eq .Zoom 0.8}}SELECTED{{end}}>0.8 x</OPTION>
|
||||
<OPTION VALUE="0.9" {{ if eq .Zoom 0.9}}SELECTED{{end}}>0.9 x</OPTION>
|
||||
<OPTION VALUE="1.0" {{ if eq .Zoom 1.0}}SELECTED{{end}}>1.0 x</OPTION>
|
||||
<OPTION VALUE="1.1" {{ if eq .Zoom 1.1}}SELECTED{{end}}>1.1 x</OPTION>
|
||||
<OPTION VALUE="1.2" {{ if eq .Zoom 1.2}}SELECTED{{end}}>1.2 x</OPTION>
|
||||
<OPTION VALUE="1.3" {{ if eq .Zoom 1.3}}SELECTED{{end}}>1.3 x</OPTION>
|
||||
</SELECT>
|
||||
{{ end }}
|
||||
M <SELECT NAME="m">
|
||||
<OPTION DISABLED>Mode</OPTION>
|
||||
<OPTION VALUE="ismap" {{ if eq .WrpMode "ismap"}}SELECTED{{end}}>ISMAP</OPTION>
|
||||
<OPTION VALUE="html" {{ if eq .WrpMode "html"}}SELECTED{{end}}>HTML</OPTION>
|
||||
</SELECT>
|
||||
T <SELECT NAME="t">
|
||||
<OPTION DISABLED>Type</OPTION>
|
||||
<OPTION VALUE="gip" {{ if eq .ImgType "gip"}}SELECTED{{end}}>GIP</OPTION>
|
||||
<OPTION VALUE="png" {{ if eq .ImgType "png"}}SELECTED{{end}}>PNG</OPTION>
|
||||
<OPTION VALUE="gif" {{ if eq .ImgType "gif"}}SELECTED{{end}}>GIF</OPTION>
|
||||
<OPTION VALUE="jpg" {{ if eq .ImgType "jpg"}}SELECTED{{end}}>JPG</OPTION>
|
||||
</SELECT>
|
||||
{{ if eq .ImgType "gif" }}
|
||||
C <SELECT NAME="c">
|
||||
<OPTION DISABLED>Ncol</OPTION>
|
||||
<OPTION VALUE="256" {{ if eq .NColors 256}}SELECTED{{end}}>256</OPTION>
|
||||
<OPTION VALUE="216" {{ if eq .NColors 216}}SELECTED{{end}}>216</OPTION>
|
||||
<OPTION VALUE="128" {{ if eq .NColors 128}}SELECTED{{end}}>128</OPTION>
|
||||
<OPTION VALUE="64" {{ if eq .NColors 64}}SELECTED{{end}}>64</OPTION>
|
||||
<OPTION VALUE="16" {{ if eq .NColors 16}}SELECTED{{end}}>16</OPTION>
|
||||
<OPTION VALUE="2" {{ if eq .NColors 2}}SELECTED{{end}}>2</OPTION>
|
||||
</SELECT>
|
||||
{{ end }}
|
||||
{{ if eq .ImgType "jpg" }}
|
||||
Q <INPUT TYPE="TEXT" NAME="q" VALUE="{{.JQual}}" SIZE="2">%
|
||||
{{ end }}
|
||||
{{ if eq .WrpMode "ismap" }}
|
||||
K <INPUT TYPE="TEXT" NAME="k" VALUE="" SIZE="4">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="Bs">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="Rt"><!--
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="All">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="<">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="^">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE="v">
|
||||
<INPUT TYPE="SUBMIT" NAME="Fn" VALUE=">" SIZE="1">-->
|
||||
{{ end }}
|
||||
</FORM>
|
||||
<BR>
|
||||
{{if .ImgURL}}
|
||||
<A HREF="{{.MapURL}}">
|
||||
<IMG SRC="{{.ImgURL}}" BORDER="0" ALT="Url: {{.URL}}, Size: {{.ImgSize}} PageHeight: {{.PageHeight}}" WIDTH="{{.ImgWidth}}" HEIGHT="{{.ImgHeight}}" ISMAP>
|
||||
</A>
|
||||
<P>
|
||||
{{end}}
|
||||
{{.TeXT}}
|
||||
<FONT SIZE="-2">
|
||||
<A HREF="/?url=https://github.com/tenox7/wrp/&w={{.Width}}&h={{.Height}}&s={{printf "%.1f" .Zoom}}&c={{.NColors}}&t={{.ImgType}}">Web Rendering Proxy {{.Version}}</A> |
|
||||
<A HREF="/shutdown/">Shutdown WRP</A> |
|
||||
{{ if eq .WrpMode "ismap" }}
|
||||
<A HREF="/">Page Height: {{.PageHeight}}</A> |
|
||||
<A HREF="/">Img Size: {{.ImgSize}}</A>
|
||||
{{end}}
|
||||
</FONT>
|
||||
</BODY>
|
||||
</HTML>
|
||||
Reference in New Issue
Block a user