mirror of
https://github.com/tenox7/wrp.git
synced 2026-02-11 13:45:48 +00:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4848d235 | ||
|
|
06317022a6 | ||
|
|
69d4b39eff | ||
|
|
5f6a1154df | ||
|
|
d6005b52fd | ||
|
|
fabcd721c3 | ||
|
|
0ee45139c3 | ||
|
|
936cb97bc0 | ||
|
|
02758bd039 | ||
|
|
a05a30c26f | ||
|
|
9c96a62816 | ||
|
|
5dd4b5feab | ||
|
|
791e87d7ed | ||
|
|
a8cc1b6b4e | ||
|
|
9358691ce5 | ||
|
|
253d36e963 | ||
|
|
719a7fc560 | ||
|
|
3270bbcdd3 | ||
|
|
6dfe7ddafc | ||
|
|
a6df4cbec4 | ||
|
|
e48f0c9ff2 | ||
|
|
c7fcea908f | ||
|
|
b91bbed4a7 | ||
|
|
7a2f673fd0 | ||
|
|
61b84116b1 | ||
|
|
ea738f206a | ||
|
|
981055dff9 | ||
|
|
deb0cf7923 | ||
|
|
186fda4949 | ||
|
|
7610f52574 | ||
|
|
416490289d | ||
|
|
0ae49044c2 | ||
|
|
cb87a83d26 | ||
|
|
ebe19912e6 | ||
|
|
5d8f51ac66 | ||
|
|
d905704a2a | ||
|
|
d382c38547 | ||
|
|
12664e6a10 | ||
|
|
e643ec1d69 | ||
|
|
9ad651c72c | ||
|
|
a897f76e20 | ||
|
|
df400d57b3 | ||
|
|
546e686cbc | ||
|
|
57a107aa69 | ||
|
|
a3eb7cb69a | ||
|
|
d8617af9c2 | ||
|
|
c6186d6fb4 | ||
|
|
7d84d01268 | ||
|
|
5b827bffb0 | ||
|
|
0680b4a72e | ||
|
|
e869291f8e | ||
|
|
071a75dfc6 | ||
|
|
34d4e2fa2a | ||
|
|
2ac4464b98 | ||
|
|
cc3d4a674b | ||
|
|
6655993c07 | ||
|
|
64cf34fb85 | ||
|
|
85063cefc8 | ||
|
|
7013f521b3 | ||
|
|
b15d64351b | ||
|
|
0b617d1592 | ||
|
|
f6446c0a3e | ||
|
|
4526dfca64 | ||
|
|
40c0329d1a | ||
|
|
67ec48aa62 | ||
|
|
b89211e432 | ||
|
|
ef96884e77 | ||
|
8549e13f64
|
|||
|
6a6d559a74
|
|||
|
5c1559984d
|
|||
|
18b382f82d
|
|||
| b090ea47b0 | |||
|
20544c2d50
|
|||
|
12496f3d7c
|
|||
|
fb569d0a62
|
|||
|
b4333ca2fd
|
|||
|
9914700ae9
|
|||
|
100ab83720
|
|||
|
4fefe187cd
|
|||
|
762a3822f7
|
|||
|
afc5e5724d
|
|||
|
1e79b37d7c
|
|||
|
90d1e82b00
|
|||
|
e34a63604f
|
|||
|
d7a2db49b8
|
|||
|
6ea543b077
|
|||
|
6854596629
|
|||
|
2faee75543
|
|||
|
b4b730b652
|
|||
|
3d5d56c5f7
|
|||
|
29c71e6fd9
|
|||
|
ebb8d0efdf
|
|||
|
85f9b175ce
|
|||
|
966923949b
|
|||
|
4ebed0ad60
|
|||
|
|
b463aad8ca
|
||
|
f91e9ce356
|
|||
|
e209d42e30
|
|||
|
|
8a431f4668 | ||
|
|
f827d20300 | ||
|
|
e1891e2e37 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vscode
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
22
Makefile
Normal file
22
Makefile
Normal file
@@ -0,0 +1,22 @@
|
||||
all: linux freebsd openbsd macos windows rpi
|
||||
|
||||
linux:
|
||||
GOOS=linux GOARCH=amd64 go build -a -o wrp-linux wrp.go
|
||||
|
||||
freebsd:
|
||||
GOOS=freebsd GOARCH=amd64 go build -a -o wrp-freebsd wrp.go
|
||||
|
||||
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
|
||||
|
||||
rpi:
|
||||
GOOS=linux GOARCH=arm go build -a -o wrp-linux-rpi wrp.go
|
||||
|
||||
clean:
|
||||
rm -rf wrp-linux wrp-freebsd wrp-openbsd wrp-macos wrp-windows.exe wrp-linux-rpi
|
||||
3
README
3
README
@@ -1,3 +0,0 @@
|
||||
WRP is a HTTP proxy service that renders the web page in to a GIF/JPEG image associated with clickable imagemap of the original web links. It allows to use historical and obsolete web browsers on the modern web. It's still a work in progress but it's quite stable and usable for casual web browsing.
|
||||
|
||||
Two versions of WRP exist, Cocoa-Webkit for Mac OS X and QT-Webkit, for Linux, BSD and other platforms.
|
||||
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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 associated with clickable imagemap of original web links.
|
||||
|
||||
**You are looking at a GoLang / CDP branch of WRP.**
|
||||
|
||||
**This code is under active development and not fully usable yet.**
|
||||
|
||||
## Done so far
|
||||
|
||||
* basic browser-in-browser mode
|
||||
* screenshot and serve image+map via CDP
|
||||
* gif with Floyd–Steinberg dithering
|
||||
* multiple concurent client support
|
||||
* resolve relative links
|
||||
* paginated scrolling
|
||||
* google search on input not starting with ^http
|
||||
* ISMAP, although for a redirect to work `-i` flag must be specified
|
||||
otherwise http-equiv refresh will be used and/or link provided
|
||||
* headed mode and chromedp debug output
|
||||
|
||||
## Todo
|
||||
|
||||
* configurable color palete and quantization
|
||||
* real http proxy support
|
||||
* padded box model coordinates
|
||||
* better http server shutdown
|
||||
|
||||
## Old Python version
|
||||
|
||||
Check [pywebkit/](/pywebkit) folder for the old Python-Webkit version.
|
||||
36
pywebkit/Changelog.md
Normal file
36
pywebkit/Changelog.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## [2.0] - 2017-05-10
|
||||
### Added
|
||||
- Support PyQt5 if available.
|
||||
- Sets title from original one.
|
||||
- Returns server errors as is.
|
||||
- Download non-HTML files as is.
|
||||
- For JavaScript capable browsers detect and automatically set view width.
|
||||
- Add support for configuring which image format to use.
|
||||
- Added support for PythonMagick. If found, allows to dither, color-reduce, or convert to grayscale or monochrome.
|
||||
- If PythonMagick is found, render as PNG and convert to user-requested format using it, for better quality.
|
||||
|
||||
### Changed
|
||||
- Support www prepented to http://wrp.stop command.
|
||||
|
||||
### Fixed
|
||||
- Prevent python crashes with non-ASCII character in URLs.
|
||||
|
||||
## [1.4] - 2017-01-22
|
||||
### Added
|
||||
- Suport for ISMAP on Linux.
|
||||
- Use queues instead of globals in Linux.
|
||||
|
||||
## [1.3] - 2017-01-21
|
||||
### Changed
|
||||
- Merged mac OS and Linux in a single executable.
|
||||
- Use queues instead of globals in Linux.
|
||||
|
||||
### Fixed
|
||||
- Call PyQt to close application on http://wrp.stop
|
||||
|
||||
## [1.2] - 2016-12-27
|
||||
### Added
|
||||
- Support for IMAP on mac OS.
|
||||
|
||||
### Changed
|
||||
- Use queues instead of globals in mac OS.
|
||||
29
pywebkit/README.md
Normal file
29
pywebkit/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 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/PNG/JPEG image associated with clickable imagemap of original web links.
|
||||
|
||||
|
||||
# Current Status
|
||||
* This is a WebKit / Python version of WRP.
|
||||
* No longer maintained / supported.
|
||||
* You should be using GoLang/CDP version instead.
|
||||
* It mostly works for casual browsing but it's not very stable.
|
||||
* Secure aka https/SSL/TLS websites might work with use of [sslstrip](https://moxie.org/software/sslstrip/) cheat (enabled by default).
|
||||
|
||||
|
||||
## OS Support
|
||||
WRP works on macOS (Mac OS X), Linux and FreeBSD. On macOS it uses Cocoa Webkit, on Linux/FreeBSD QT Webkit, for which needs PyQT4 or PyQT5. It does not work on Windows. Use Go/CDP version for that.
|
||||
|
||||
## Installation
|
||||
* macOS - should just work
|
||||
* Linux/FreeBSD install `python-pyqt5.qtwebkit` and `sslstrip`
|
||||
* For PythonMagick (Imagemagick library) install `python-pythonmagick`
|
||||
|
||||
## Configuration
|
||||
Edit wrp.py, scroll past Copyright section to find config parameters
|
||||
|
||||
## Usage
|
||||
Configure your web browser to use HTTP proxy at IP address and port where WRP is running. If using browsers prior to HTML 3.2, ISMAP option may need to be enabled. Check configuration.
|
||||
|
||||
## 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/
|
||||
931
pywebkit/wrp.py
Normal file
931
pywebkit/wrp.py
Normal file
@@ -0,0 +1,931 @@
|
||||
#!/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))
|
||||
|
||||
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()
|
||||
228
wrp-cocoa.py
228
wrp-cocoa.py
@@ -1,228 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# wrp.py - Web Rendering Proxy
|
||||
# A HTTP proxy service that renders the requested URL in to a GIF image associated
|
||||
# with an imagemap of clickable links. This is an adaptation of previous works by
|
||||
# picidae.net and Paul Hammond.
|
||||
|
||||
__version__ = "1.1"
|
||||
|
||||
#
|
||||
# This program is based on the software picidae.py from picidae.net
|
||||
# It was modified by Antoni Sawicki http://www.tenox.net/out/#wrp
|
||||
#
|
||||
# This program is based on the software webkit2png from Paul Hammond.
|
||||
# It was extended by picidae.net
|
||||
#
|
||||
# Copyright (c) 2013-2014 Antoni Sawicki
|
||||
# Copyright (c) 2012-2013 picidae.net
|
||||
# Copyright (c) 2004-2013 Paul Hammond
|
||||
#
|
||||
# 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
|
||||
|
||||
import re
|
||||
import random
|
||||
import Foundation
|
||||
import WebKit
|
||||
import AppKit
|
||||
import objc
|
||||
import os
|
||||
import time
|
||||
import string
|
||||
import urllib
|
||||
import socket
|
||||
import SocketServer
|
||||
import SimpleHTTPServer
|
||||
import threading
|
||||
import Queue
|
||||
|
||||
# Request queue (URLs go in here)
|
||||
REQ = Queue.Queue()
|
||||
# Response queue (dummy response objects)
|
||||
RESP = Queue.Queue()
|
||||
|
||||
#import pdb; pdb.set_trace()
|
||||
|
||||
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):
|
||||
rurl = REQ.get()
|
||||
|
||||
if (rurl == "http://wrp.stop/"):
|
||||
print ">>> Terminate Request Received"
|
||||
AppKit.NSApplication.sharedApplication().terminate_(None)
|
||||
|
||||
nsurl = Foundation.NSURL.URLWithString_(rurl)
|
||||
if not (nsurl and nsurl.scheme()):
|
||||
nsurl = Foundation.NSURL.alloc().initFileURLWithPath_(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()
|
||||
|
||||
bitmapdata = self.captureView(view)
|
||||
bitmapdata.representationUsingType_properties_(AppKit.NSGIFFileType,None).writeToFile_atomically_(GIF,objc.YES)
|
||||
|
||||
httpout.write("<!-- Web Rendering Proxy v%s by Antoni Sawicki -->\n<html>\n<body>\n<img src=\"http://%s\" alt=\"webrender\" usemap=\"#map\">\n<map name=\"map\">\n" % (__version__, GIF))
|
||||
|
||||
domdocument = frame.DOMDocument()
|
||||
domnodelist = domdocument.getElementsByTagName_('A')
|
||||
i = 0
|
||||
while i < domnodelist.length():
|
||||
value = domnodelist.item_(i).valueForKey_('href')
|
||||
myrect = domnodelist.item_(i).boundingBox()
|
||||
|
||||
xmin = Foundation.NSMinX(myrect)
|
||||
ymin = Foundation.NSMinY(myrect)
|
||||
xmax = Foundation.NSMaxX(myrect)
|
||||
ymax = Foundation.NSMaxY(myrect)
|
||||
|
||||
httpout.write("<area shape=\"rect\" coords=\"%i,%i,%i,%i\" alt=\"%s\" href=\"%s\">\n" % (xmin, ymin, xmax, ymax, value, value))
|
||||
i += 1
|
||||
|
||||
httpout.write("</map>\n</body>\n</html>\n")
|
||||
|
||||
RESP.put('')
|
||||
self.getURL(webview)
|
||||
|
||||
class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
req_url=self.path
|
||||
global httpout
|
||||
httpout=self.wfile
|
||||
self.send_response(200, 'OK')
|
||||
|
||||
gif_re = re.compile("http://webrender-[0-9]+\.gif")
|
||||
ico_re = re.compile(".+\.ico")
|
||||
|
||||
if (gif_re.search(req_url)):
|
||||
img=req_url.split("/")
|
||||
print ">>> request for rendered gif image... %s" % (img[2])
|
||||
self.send_header('Content-type', 'image/gif')
|
||||
self.end_headers()
|
||||
fimg = open(img[2])
|
||||
httpout.write(fimg.read())
|
||||
fimg.close()
|
||||
os.remove(img[2])
|
||||
|
||||
elif (ico_re.search(req_url)):
|
||||
#print ">>> request for .ico file - skipping"
|
||||
self.send_error(404, "ICO not supported")
|
||||
self.end_headers()
|
||||
|
||||
else:
|
||||
print ">>> request for url: " + req_url
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
global GIF
|
||||
GIF = "webrender-%s.gif" % (random.randrange(0,1000))
|
||||
|
||||
# To thread
|
||||
REQ.put(req_url)
|
||||
# Wait for completition
|
||||
RESP.get()
|
||||
|
||||
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():
|
||||
# Launch Proxy Thread
|
||||
threading.Thread(target=run_proxy).start()
|
||||
|
||||
# Launch NS Application
|
||||
AppKit.NSApplicationLoad();
|
||||
app = AppKit.NSApplication.sharedApplication()
|
||||
delegate = AppDelegate.alloc().init()
|
||||
AppKit.NSApp().setDelegate_(delegate)
|
||||
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()
|
||||
|
||||
if __name__ == '__main__' : main()
|
||||
|
||||
422
wrp-qt.py
422
wrp-qt.py
@@ -1,422 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Web Rendering Proxy (qt-webkit) by Antoni Sawicki - http://www.tenox.net/out/#wrp
|
||||
# HTTP proxy service that renders the web page in to a JPEG image
|
||||
# associated with clickable imagemap of the original web links
|
||||
# This version works only with QT-Webkit (eg.: Linux, BSD, others)
|
||||
#
|
||||
# This program is loosely based on the following software:
|
||||
# Adam Nelson webkit2png: https://github.com/adamn/python-webkit2png
|
||||
# Roland Tapken: http://www.blogs.uni-osnabrueck.de/rotapken/2008/12/03/create-screenshots-of-a-web-page-using-python-and-qtwebkit/
|
||||
# picidae.py from picidae.net: https://code.google.com/p/phantomjs/issues/attachmentText?id=209&aid=2090003000&name=picidae.py
|
||||
# Paul Hammond webkit2png: http://www.paulhammond.org/webkit2png/
|
||||
#
|
||||
# Copyright (c) 2013-2014 Antoni Sawicki
|
||||
# Copyright (c) 2012 picidae.net
|
||||
# Copyright (c) 2008 Roland Tapken
|
||||
# Copyright (c) 2004-2014 Paul Hammond
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
#
|
||||
|
||||
#
|
||||
# Configuration options:
|
||||
#
|
||||
PORT = 8080
|
||||
WIDTH = 0 # eg.: 640, 800, 1024, 0 for auto
|
||||
HEIGHT = 0 # eg.: 480, 600, 768, 0 for auto
|
||||
WAIT = 1 # sleep for 1 second to allow javascript renders
|
||||
QUALITY = 80 # jpeg image quality 0-100
|
||||
|
||||
__version__ = "1.1qt"
|
||||
|
||||
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 signal
|
||||
import logging
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtWebKit import *
|
||||
from PyQt4.QtNetwork import *
|
||||
|
||||
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
|
||||
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()
|
||||
image = QPixmap.grabWindow(self._window.winId())
|
||||
else:
|
||||
image = QPixmap.grabWidget(self._window)
|
||||
|
||||
# Write URL map
|
||||
httpout.write("<!-- Web Rendering Proxy v%s by Antoni Sawicki -->\n<html>\n<body>\n<img src=\"http://%s\" alt=\"webrender\" usemap=\"#map\">\n<map name=\"map\">\n" % (__version__, IMG))
|
||||
frame = self._view.page().currentFrame()
|
||||
for x in frame.findAllElements('a'):
|
||||
value = x.attribute('href')
|
||||
xmin, ymin, xmax, ymax = x.geometry().getCoords()
|
||||
httpout.write("<area shape=\"rect\" coords=\"%i,%i,%i,%i\" alt=\"%s\" href=\"%s\">\n" % (xmin, ymin, xmax, ymax, value, value))
|
||||
httpout.write("</map>\n</body>\n</html>\n")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Request queue (URLs go in here)
|
||||
REQ = Queue.Queue()
|
||||
# Response queue (dummy response objects)
|
||||
RESP = Queue.Queue()
|
||||
|
||||
#import pdb; pdb.set_trace()
|
||||
|
||||
class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
req_url=self.path
|
||||
global httpout
|
||||
httpout=self.wfile
|
||||
self.send_response(200, 'OK')
|
||||
|
||||
jpg_re = re.compile("http://webrender-[0-9]+\.jpg")
|
||||
ico_re = re.compile(".+\.ico")
|
||||
|
||||
if (jpg_re.search(req_url)):
|
||||
img=req_url.split("/")
|
||||
print ">>> request for rendered jpg image... %s [%d kb]" % (img[2], os.path.getsize(img[2])/1024)
|
||||
self.send_header('Content-type', 'image/jpeg')
|
||||
self.end_headers()
|
||||
fimg = open(img[2])
|
||||
httpout.write(fimg.read())
|
||||
fimg.close()
|
||||
os.remove(img[2])
|
||||
|
||||
elif (ico_re.search(req_url)):
|
||||
print ">>> request for .ico file - skipping"
|
||||
self.send_error(404, "ICO not supported")
|
||||
self.end_headers()
|
||||
|
||||
else:
|
||||
print ">>> request for url: " + req_url
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
global IMG
|
||||
IMG = "webrender-%s.jpg" % (random.randrange(0,1000))
|
||||
|
||||
# To thread
|
||||
REQ.put(req_url)
|
||||
# Wait for completition
|
||||
RESP.get()
|
||||
|
||||
def run_proxy():
|
||||
httpd = SocketServer.TCPServer(('', PORT), Proxy)
|
||||
print "Web Rendering Proxy v%s serving port: %s" % (__version__, PORT)
|
||||
while 1:
|
||||
httpd.serve_forever()
|
||||
|
||||
def main():
|
||||
# Launch Proxy Thread
|
||||
threading.Thread(target=run_proxy).start()
|
||||
|
||||
# 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:
|
||||
rurl = REQ.get()
|
||||
if rurl == "http://wrp.stop/":
|
||||
print ">>> Terminate Request Received"
|
||||
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(rurl)
|
||||
qBuffer = QBuffer()
|
||||
image.save(qBuffer, 'jpg', QUALITY)
|
||||
|
||||
output = open(IMG, 'w')
|
||||
output.write(qBuffer.buffer().data())
|
||||
output.close()
|
||||
|
||||
del renderer
|
||||
print ">>> done: %s [%d kb]..." % (IMG, os.path.getsize(IMG)/1024)
|
||||
|
||||
RESP.put('')
|
||||
|
||||
QApplication.exit(0)
|
||||
except RuntimeError, e:
|
||||
logger.error("main: %s" % e)
|
||||
print >> sys.stderr, e
|
||||
QApplication.exit(1)
|
||||
|
||||
# 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_())
|
||||
|
||||
if __name__ == '__main__' : main()
|
||||
287
wrp.go
Normal file
287
wrp.go
Normal file
@@ -0,0 +1,287 @@
|
||||
//
|
||||
// WRP - Web Rendering Proxy
|
||||
//
|
||||
// Copyright (c) 2013-2018 Antoni Sawicki
|
||||
// Copyright (c) 2019 Google LLC
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
_ "image"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// Ismap for server side processing
|
||||
type Ismap struct {
|
||||
xmin int64
|
||||
ymin int64
|
||||
xmax int64
|
||||
ymax int64
|
||||
url string
|
||||
}
|
||||
|
||||
var (
|
||||
version = "3.0"
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
gifmap = make(map[string]bytes.Buffer)
|
||||
ismap = make(map[string][]Ismap)
|
||||
)
|
||||
|
||||
func pageServer(out http.ResponseWriter, req *http.Request) {
|
||||
req.ParseForm()
|
||||
u := req.FormValue("url")
|
||||
var istr string
|
||||
var ion string
|
||||
var i bool
|
||||
if req.FormValue("i") == "on" {
|
||||
istr = "CHECKED"
|
||||
i = true
|
||||
ion = "&i=on"
|
||||
} else {
|
||||
istr = ""
|
||||
i = false
|
||||
}
|
||||
p, _ := strconv.ParseInt(req.FormValue("p"), 10, 64)
|
||||
if req.FormValue("pg") == "Dn" {
|
||||
p++
|
||||
} else if req.FormValue("pg") == "Up" {
|
||||
p--
|
||||
} else {
|
||||
p = 0
|
||||
}
|
||||
w, _ := strconv.ParseInt(req.FormValue("w"), 10, 64)
|
||||
if w < 10 {
|
||||
w = 1024
|
||||
}
|
||||
h, _ := strconv.ParseInt(req.FormValue("h"), 10, 64)
|
||||
if h < 10 {
|
||||
h = 768
|
||||
}
|
||||
s, _ := strconv.ParseFloat(req.FormValue("s"), 64)
|
||||
if s < 0.1 {
|
||||
s = 1.0
|
||||
}
|
||||
c, _ := strconv.ParseInt(req.FormValue("c"), 10, 64)
|
||||
if c < 2 || c > 256 {
|
||||
c = 256
|
||||
}
|
||||
log.Printf("%s Page Reqest for url=\"%s\" [%s]\n", req.RemoteAddr, u, req.URL.Path)
|
||||
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", u)
|
||||
fmt.Fprintf(out, "<FORM ACTION=\"/\"><INPUT TYPE=\"TEXT\" NAME=\"url\" VALUE=\"%s\" SIZE=\"20\">", u)
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" VALUE=\"Go\"> \n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"pg\" VALUE=\"Up\"> \n")
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"TEXT\" NAME=\"p\" VALUE=\"%d\" SIZE=\"2\"> \n", p)
|
||||
fmt.Fprintf(out, "<INPUT TYPE=\"SUBMIT\" NAME=\"pg\" VALUE=\"Dn\"> \n")
|
||||
fmt.Fprintf(out, "I <INPUT TYPE=\"CHECKBOX\" NAME=\"i\" %s> \n", istr)
|
||||
fmt.Fprintf(out, "W <INPUT TYPE=\"TEXT\" NAME=\"w\" VALUE=\"%d\" SIZE=\"4\"> \n", w)
|
||||
fmt.Fprintf(out, "H <INPUT TYPE=\"TEXT\" NAME=\"h\" VALUE=\"%d\" SIZE=\"4\"> \n", h)
|
||||
fmt.Fprintf(out, "S <INPUT TYPE=\"TEXT\" NAME=\"s\" VALUE=\"%1.2f\" SIZE=\"3\"> \n", s)
|
||||
fmt.Fprintf(out, "C <INPUT TYPE=\"TEXT\" NAME=\"c\" VALUE=\"%d\" SIZE=\"3\"> \n", c)
|
||||
fmt.Fprintf(out, "</FORM><BR>\n")
|
||||
if len(u) > 1 {
|
||||
if strings.HasPrefix(u, "http") {
|
||||
capture(u, w, h, s, int(c), p, i, req.RemoteAddr, out)
|
||||
} else {
|
||||
capture(fmt.Sprintf("http://www.google.com/search?q=%s", url.QueryEscape(u)), w, h, s, int(c), p, i, req.RemoteAddr, out)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(out, "No URL or search query specified")
|
||||
}
|
||||
fmt.Fprintf(out, "\n<P><A HREF=\"/?url=https://github.com/tenox7/wrp/&w=%d&h=%d&s=%1.2f&c=%d%s\">Web Rendering Proxy Version %s</A></BODY>\n</HTML>\n", w, h, s, c, ion, version)
|
||||
}
|
||||
|
||||
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 mapServer(out http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s ISMAP Request for %s [%+v]\n", req.RemoteAddr, req.URL.Path, req.URL.RawQuery)
|
||||
var loc string
|
||||
var x, y int64
|
||||
n, err := fmt.Sscanf(req.URL.RawQuery, "%d,%d", &x, &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
|
||||
}
|
||||
is, ok := ismap[req.URL.Path]
|
||||
if !ok || is == nil {
|
||||
fmt.Fprintf(out, "Unable to find map %s\n", req.URL.Path)
|
||||
log.Printf("Unable to find map %s\n", req.URL.Path)
|
||||
return
|
||||
}
|
||||
defer delete(ismap, req.URL.Path)
|
||||
for _, i := range is {
|
||||
if x >= i.xmin && x <= i.xmax && y >= i.ymin && y <= i.ymax {
|
||||
loc = i.url
|
||||
}
|
||||
}
|
||||
if len(loc) < 1 {
|
||||
loc = is[0].url
|
||||
}
|
||||
log.Printf("%s ISMAP Redirect to: http://%s%s\n", req.RemoteAddr, req.Context().Value(http.LocalAddrContextKey), loc)
|
||||
http.Redirect(out, req, fmt.Sprintf("http://%s%s", req.Context().Value(http.LocalAddrContextKey), loc), 301)
|
||||
}
|
||||
|
||||
func capture(gourl string, w int64, h int64, s float64, co int, p int64, i bool, c string, out http.ResponseWriter) {
|
||||
var nodes []*cdp.Node
|
||||
ctxx := chromedp.FromContext(ctx)
|
||||
var pngbuf []byte
|
||||
var gifbuf bytes.Buffer
|
||||
var loc string
|
||||
var res *runtime.RemoteObject
|
||||
is := make([]Ismap, 0)
|
||||
var ion string
|
||||
|
||||
log.Printf("%s Processing Caputure Request for %s\n", c, gourl)
|
||||
|
||||
// Run ChromeDP Magic
|
||||
err := chromedp.Run(ctx,
|
||||
emulation.SetDeviceMetricsOverride(int64(float64(w)/s), int64(float64(h)/s), s, false),
|
||||
chromedp.Navigate(gourl),
|
||||
chromedp.Evaluate(fmt.Sprintf("window.scrollTo(0, %d);", p*int64(float64(h)*float64(0.9))), &res),
|
||||
chromedp.Sleep(time.Second*1),
|
||||
chromedp.CaptureScreenshot(&pngbuf),
|
||||
chromedp.Location(&loc),
|
||||
chromedp.Nodes("a", &nodes, chromedp.ByQueryAll))
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s %s", c, err)
|
||||
fmt.Fprintf(out, "<BR>%s<BR>", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("%s Landed on: %s, Nodes: %d\n", c, loc, len(nodes))
|
||||
|
||||
// Process Screenshot Image
|
||||
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: co})
|
||||
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
|
||||
}
|
||||
seq := rand.Intn(9999)
|
||||
imgpath := fmt.Sprintf("/img/%04d.gif", seq)
|
||||
mappath := fmt.Sprintf("/map/%04d.map", seq)
|
||||
log.Printf("%s Encoded GIF image: %s, Size: %dKB\n", c, imgpath, len(gifbuf.Bytes())/1024)
|
||||
gifmap[imgpath] = gifbuf
|
||||
|
||||
// Process Nodes
|
||||
base, _ := url.Parse(loc)
|
||||
if i {
|
||||
fmt.Fprintf(out, "<A HREF=\"%s\"><IMG SRC=\"%s\" ALT=\"wrp\" BORDER=\"0\" ISMAP></A>", mappath, imgpath)
|
||||
is = append(is, Ismap{xmin: -1, xmax: -1, ymin: -1, ymax: -1, url: fmt.Sprintf("/?url=%s&w=%d&h=%d&s=%1.2f&c=%d&i=on", loc, w, h, s, co)})
|
||||
ion = "&i=on"
|
||||
} else {
|
||||
fmt.Fprintf(out, "<IMG SRC=\"%s\" ALT=\"wrp\" BORDER=\"0\" USEMAP=\"#map\">\n<MAP NAME=\"map\">\n", imgpath)
|
||||
}
|
||||
|
||||
for _, n := range nodes {
|
||||
b, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(cdp.WithExecutor(ctx, ctxx.Target))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tgt, err := base.Parse(n.AttributeValue("href"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
target := fmt.Sprintf("/?url=%s&w=%d&h=%d&s=%1.2f&c=%d%s", tgt, w, h, s, co, ion) // no page# here
|
||||
|
||||
if len(b.Content) > 6 && len(target) > 7 {
|
||||
if i {
|
||||
is = append(is, Ismap{
|
||||
xmin: int64(b.Content[0] * s), ymin: int64(b.Content[1] * s),
|
||||
xmax: int64(b.Content[4] * s), ymax: int64(b.Content[5] * s),
|
||||
url: target})
|
||||
} else {
|
||||
fmt.Fprintf(out, "<AREA SHAPE=\"RECT\" COORDS=\"%.f,%.f,%.f,%.f\" ALT=\"%s\" TITLE=\"%s\" HREF=\"%s\">\n",
|
||||
b.Content[0]*s, b.Content[1]*s, b.Content[4]*s, b.Content[5]*s, n.AttributeValue("href"), n.AttributeValue("href"), target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i {
|
||||
log.Printf("%s Encoded ISMAP %s\n", c, mappath)
|
||||
} else {
|
||||
fmt.Fprintf(out, "</MAP>\n")
|
||||
}
|
||||
out.(http.Flusher).Flush()
|
||||
log.Printf("%s Done with caputure for %s\n", c, gourl)
|
||||
ismap[mappath] = is
|
||||
}
|
||||
|
||||
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")
|
||||
flag.Parse()
|
||||
if head {
|
||||
headless = false
|
||||
} else {
|
||||
headless = true
|
||||
}
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", headless),
|
||||
)
|
||||
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)
|
||||
}
|
||||
defer cancel()
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
http.HandleFunc("/", pageServer)
|
||||
http.HandleFunc("/img/", imgServer)
|
||||
http.HandleFunc("/map/", mapServer)
|
||||
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)
|
||||
http.ListenAndServe(addr, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user