220 Commits

Author SHA1 Message Date
Kevin Pham
2dcc832993 fix ruleset application bug where rulesets that apply other rulesets are not applied on a single proxychain pass 2023-12-16 14:13:55 -06:00
joncrangle
01efe42fd6 Remove pnpm watch from pre_cmd 2023-12-16 11:49:34 -05:00
joncrangle
251b7e3b55 Add tailwind watch to air; use tailwind native minify 2023-12-16 01:16:44 -05:00
Kevin Pham
88dce971ff tweak error page middleware to return proper json response if on API path 2023-12-15 08:04:09 -06:00
joncrangle
a1b6230f17 Merge branch 'origin/proxy_v2' of https://github.com/everywall/ladder into origin/proxy_v2 2023-12-14 21:53:35 -05:00
joncrangle
e4155f1d07 Undo uncomment of chain._reset 2023-12-14 21:53:33 -05:00
Github action
909bca5a72 Generated stylesheet 2023-12-15 02:51:09 +00:00
joncrangle
e8255bd3a7 Use CreateAPIErrReader for error_page middleware 2023-12-14 21:50:48 -05:00
Kevin Pham
b8fb930987 Merge pull request #69 from everywall/errorpage
Render error page when accept header is text/html
2023-12-12 23:41:10 -06:00
joncrangle
ee26eafb3e Simpler error return for non text/html 2023-12-13 00:25:14 -05:00
Github action
8947647116 Generated stylesheet 2023-12-13 05:07:02 +00:00
joncrangle
f901e4aee0 Common footer to outline page 2023-12-13 00:06:35 -05:00
joncrangle
5492921fef Common footer to outline page 2023-12-13 00:06:15 -05:00
joncrangle
10d4b6818e Undo print view after printing, a11y, styling and prettier 2023-12-13 00:05:51 -05:00
joncrangle
ad415bee8c Add error page when accept header is text/html 2023-12-13 00:04:19 -05:00
joncrangle
c2d6a2c461 Generate styles 2023-12-13 00:03:23 -05:00
joncrangle
02eb06995c Generate struct defs 2023-12-13 00:03:10 -05:00
Kevin Pham
0940bbb21b Merge pull request #67 from joncrangle/proxy_v2/playground
feat: Playground
2023-12-11 14:24:43 -06:00
joncrangle
70033b8685 Update import to url 2023-12-10 23:48:21 -05:00
Github action
39852011e6 Generated stylesheet 2023-12-11 04:45:04 +00:00
joncrangle
6a9d010e36 Merge branch 'origin/proxy_v2' into proxy_v2/playground 2023-12-10 23:44:46 -05:00
joncrangle
16df26c3a5 Remove contribute rules link 2023-12-10 01:18:13 -05:00
Github action
9159d44c1e Generated stylesheet 2023-12-10 06:17:33 +00:00
joncrangle
8c741f0401 Link to playground 2023-12-10 01:17:13 -05:00
joncrangle
0082acf7c9 Update tags to avoid devtools issues 2023-12-10 00:39:01 -05:00
joncrangle
6effb51da6 fix: styling bug in code / textarea 2023-12-10 00:23:24 -05:00
joncrangle
1e41e6f2b0 Export to yaml 2023-12-09 23:51:36 -05:00
Kevin Pham
82b5a74d8c begin migrating old v1 rulesets to v2 rulesets 2023-12-09 16:35:54 -06:00
Kevin Pham
f165a406f1 update ruleset 2023-12-09 15:45:22 -06:00
Kevin Pham
22533dc739 improve ruleset unmarshalling behavior if there isnt a top level rule key 2023-12-09 08:37:23 -06:00
Kevin Pham
27c3892b0b add modify incoming scripts with regex modifier 2023-12-09 08:08:36 -06:00
Github action
8ffaa5fde1 Generated stylesheet 2023-12-09 06:47:32 +00:00
joncrangle
7d379f935d Handle existing values, begin work on json2yaml 2023-12-09 01:47:08 -05:00
Github action
b0af02830b Generated stylesheet 2023-12-08 21:11:43 +00:00
Jonathan Crangle
c8a3c55ed6 Work on js syntax highlighting in textarea 2023-12-08 16:11:21 -05:00
Kevin Pham
9fa00188a6 fix date formatting in outline 2023-12-08 14:34:26 -06:00
Github action
2f6a67fc67 Generated stylesheet 2023-12-08 20:07:00 +00:00
Kevin Pham
22f0007478 improve outline UX; add print mode and reading time estimation 2023-12-08 14:06:34 -06:00
Jonathan Crangle
165ffcef89 Update title style 2023-12-08 12:29:18 -05:00
Jonathan Crangle
9533f66a13 Update title link styles 2023-12-08 12:24:36 -05:00
Github action
4d37046ff6 Generated stylesheet 2023-12-08 17:17:30 +00:00
Jonathan Crangle
de640ce08c Update title link styles 2023-12-08 12:17:02 -05:00
Github action
cc42ce10cb Generated stylesheet 2023-12-08 17:03:51 +00:00
Jonathan Crangle
b3d799e36d a11y improvements 2023-12-08 12:03:22 -05:00
Kevin Pham
81c99821f0 fix title on outline generatation if it contains a ; 2023-12-08 10:21:47 -06:00
Kevin Pham
5bb6b872ba Merge pull request #68 from everywall/fix/proxy_v2/add-package-urls
#65 use full URL in package line
2023-12-08 07:19:30 -06:00
ladddder
7661c66250 #65 use full URL in package line 2023-12-08 09:26:26 +01:00
joncrangle
b94e64a4d3 Empty array instead of empty object in null coalesce 2023-12-08 01:37:27 -05:00
joncrangle
b76a941164 Remove extra quote 2023-12-08 01:35:48 -05:00
joncrangle
424893a5a3 Undo delete -> don't merge 2023-12-08 01:04:43 -05:00
joncrangle
aabc34bac7 Delete handlers/api_modifiers_structdef.gen.go 2023-12-08 00:51:33 -05:00
joncrangle
518f4d65d2 Migrate code and utilize api/modifiers endpoint 2023-12-08 00:42:10 -05:00
Kevin Pham
8e6a6aae09 fix tailwind css builder for github action 2023-12-07 21:39:48 -06:00
joncrangle
dc82db803a Migrate js to script and make input required 2023-12-07 20:27:44 -05:00
Kevin Pham
c870cd6ba8 add list modifiers api endpoint 2023-12-07 16:17:45 -06:00
Kevin Pham
c9bb0f6c25 WIP endpoint for listing available modifiers for frontend 2023-12-07 11:41:54 -06:00
Kevin Pham
0b084f44ae cleanup handlers directory 2023-12-07 10:37:05 -06:00
Kevin Pham
3aad9cf406 improve yaml content type for display in browser; improve api ergonomics 2023-12-07 10:05:54 -06:00
Kevin Pham
4cf2f8c782 for outline path content type to always be text/html 2023-12-07 10:00:13 -06:00
Kevin Pham
df19209211 fix content-type not set regression on main proxy handler 2023-12-07 09:58:52 -06:00
Kevin Pham
1ebb486592 move ruleset and raw endpoints to /api/*, update docs, fix ruleset yaml marshal panic issue 2023-12-07 09:46:38 -06:00
Kevin Pham
be68ae7caa add gitignore for air 2023-12-07 09:24:08 -06:00
Kevin Pham
8166eb31af add ruleset path support 2023-12-07 09:23:12 -06:00
Kevin Pham
4779229b32 add raw handler support 2023-12-07 08:43:05 -06:00
Kevin Pham
6a5b85f260 add subdomain url extractor; add 3p script blocker modifier 2023-12-06 14:18:40 -06:00
Kevin Pham
0e940ec217 fix tailwind css builder path 2023-12-06 10:26:49 -06:00
Kevin Pham
9d6cedb2a5 update vendor path for generate_readable_outline.go 2023-12-06 08:59:52 -06:00
Kevin Pham
f559e7f422 Merge pull request #62 from dxbednarczyk/origin/proxy_v2
Update deps and move non `*.go` files to vendor directories
2023-12-06 08:56:48 -06:00
Kevin Pham
b4dd0e5380 add localstorage clearer, sessionstorage cleaer, and cachebuster modifiers 2023-12-06 08:41:45 -06:00
Kevin Pham
6192373587 update cli flags to load ruleset 2023-12-05 21:28:53 -06:00
Damian Bednarczyk
20984438f9 move all non *.go files to vendor directory 2023-12-05 21:28:11 -06:00
Damian Bednarczyk
0bfd83f64a update deps 2023-12-05 21:27:47 -06:00
Kevin Pham
5a7fe8a70a re-add googlebot ip randomization flag to fix import cycle 2023-12-05 21:09:06 -06:00
Kevin Pham
0b33765b4f add support for rulesets in JSON format for frontend use 2023-12-05 21:03:27 -06:00
Kevin Pham
52d12dd1ac ruleset_v2 integration test working 2023-12-05 15:02:18 -06:00
Kevin Pham
e6e8b0edff Merge pull request #60 from everywall/proxy_v2_improve_docker_buildjob
use make to build binaries
2023-12-05 14:04:18 -06:00
Kevin Pham
4b04268aa7 Merge pull request #61 from everywall/proxy_v2/cleanup-main
cleanup main
2023-12-05 14:03:40 -06:00
Kevin Pham
b2f6cf9f1d finish ruleset_v2 implementation 2023-12-05 14:02:54 -06:00
Kevin Pham
9d77c63697 add ruleset_v2 loading code 2023-12-05 10:50:54 -06:00
ladddder
f2543d261e move minified tailwind css build to handler folder 2023-12-05 16:54:41 +01:00
ladddder
eef6061d93 moved styles css from main to handlers 2023-12-05 16:39:30 +01:00
ladddder
7fd2b2b3c3 move script from main to handlers 2023-12-05 16:32:42 +01:00
ladddder
f437bcabf3 move auth and favicon form main to handlers, linting 2023-12-05 16:00:31 +01:00
Gianni Carafa
a56ec1f861 use make to build binaries 2023-12-05 15:11:13 +01:00
Kevin Pham
692b2a5d17 update makefile and air config to run codegen 2023-12-05 02:04:18 -06:00
Kevin Pham
bc715d9101 implement proper marshaller/unmarshaller for rulesets in yaml format 2023-12-05 01:51:30 -06:00
Kevin Pham
f8621e72ee implement proper ruleset json serializer 2023-12-04 22:53:38 -06:00
Kevin Pham
6157d6543f add YAML serialization for ruleset 2023-12-04 19:55:50 -06:00
Kevin Pham
b39955025e Merge pull request #58 from everywall/proxy_v2_docs
Add docs for submodules
2023-12-04 11:14:42 -06:00
Kevin Pham
c28470e619 improve codegen for JSON serialization of response/request modifiers 2023-12-04 11:10:55 -06:00
ladddder
8f81583fce Add git clone command and submodule update command for development setup 2023-12-04 13:40:26 +01:00
Kevin Pham
7883b32335 work on rule reqmod resmod codegen for serialization 2023-12-03 23:34:46 -06:00
Kevin Pham
a83a95c5a5 Change form type to url #57 2023-12-03 21:34:27 -06:00
Kevin Pham
547cf61a7d begin work on refactor of ruleset and functional options serializer for proxychain response/request modifiers 2023-12-03 21:32:03 -06:00
Kevin Pham
1fc47c76b6 fix air config 2023-12-03 17:06:00 -06:00
Kevin Pham
6c0721dcb8 fix "modifer" -> "modifier" typo everywhere 2023-12-03 17:04:30 -06:00
joncrangle
117ded5653 Update button styles 2023-12-03 01:26:08 -05:00
Kevin Pham
c8a411267d Merge pull request #55 from dxbednarczyk/origin/proxy_v2
Add random IP generation for bots
2023-12-01 21:47:13 -06:00
Kevin Pham
dcdf75bad2 add response modifier to prevent the removal of elements by paywall script 2023-12-01 21:41:20 -06:00
Damian Bednarczyk
a1a2f261e9 update test to run on every ip in googlebot pool 2023-12-01 21:16:38 -06:00
Damian Bednarczyk
54173cf672 fix typo in main() and add random ip generation 2023-12-01 21:08:53 -06:00
joncrangle
d9714fb449 Error container improvement 2023-12-01 20:00:36 -05:00
Kevin Pham
59a57f2dbd tweak dynamic url rewriting logic 2023-12-01 18:45:07 -06:00
Kevin Pham
47def5c610 add patch tracker scripts response modifier 2023-12-01 16:26:59 -06:00
Kevin Pham
cdac75be06 add troubleshooting comment for CF-Connecting-IP header spoofing issue with cloudflare 2023-12-01 14:13:29 -06:00
Kevin Pham
f7ddd67b4e add patch google analytics response modifier 2023-12-01 14:12:52 -06:00
Kevin Pham
8aba4a18c2 lint 2023-12-01 13:10:37 -06:00
Kevin Pham
9f0e33aa7f Merge pull request #53 from dxbednarczyk/origin/proxy_v2
Add option to use random IPs for trusted bots
2023-12-01 13:01:57 -06:00
Kevin Pham
b996df31b2 add rulesetmap type for efficient ruleset lookup for proxychain impl 2023-12-01 12:44:12 -06:00
Github action
b9c63770a9 Generated stylesheet 2023-12-01 17:27:04 +00:00
Damian
d7b827a154 Merge branch 'origin/proxy_v2' into origin/proxy_v2 2023-12-01 11:26:44 -06:00
joncrangle
7e455c9d70 Move script tag to head 2023-12-01 01:13:44 -05:00
joncrangle
6e4bba7f91 Set theme to system in localstorage if null 2023-12-01 01:09:42 -05:00
Kevin Pham
1cd1aa805d rename 2023-11-30 22:48:06 -06:00
Kevin Pham
140a38fd39 improve /api/content metadata and tag handling 2023-11-30 22:47:15 -06:00
Kevin Pham
ef50c81aa6 improve version display start message 2023-11-30 22:14:55 -06:00
joncrangle
ac00c7f172 Build css 2023-11-30 23:14:44 -05:00
joncrangle
0aa34e2cfd Update caption styling 2023-11-30 23:14:36 -05:00
joncrangle
85be697741 Apply consistent template format 2023-11-30 22:56:44 -05:00
joncrangle
8bd4ca8a57 Ladder logo link to / 2023-11-30 22:42:50 -05:00
joncrangle
0130c010b1 Update full bin and pre_cmd 2023-11-30 22:29:47 -05:00
joncrangle
fcd8004d22 Build updated stylesheet 2023-11-30 22:29:29 -05:00
joncrangle
0fc84cbe96 Styling updates 2023-11-30 22:29:14 -05:00
joncrangle
2ad15352d6 Update base h1 and code styles 2023-11-30 22:13:28 -05:00
joncrangle
3422154e26 Fix null localstorage and h1 styling 2023-11-30 22:11:43 -05:00
joncrangle
4d5786a443 Add outline html to css build 2023-11-30 22:09:06 -05:00
joncrangle
04dfd87bd0 Specify updated content locations 2023-11-30 22:08:01 -05:00
joncrangle
d639ee68c1 Watch for changes in js 2023-11-30 22:06:52 -05:00
Kevin Pham
518a94139b author metadata in outline links to wikipedia; sitename metadata is now header 2023-11-30 20:20:43 -06:00
Github action
b4b2373b3c Generated stylesheet 2023-12-01 02:12:05 +00:00
Damian Bednarczyk
5877a49002 Merge branch 'origin/proxy_v2' of https://github.com/everywall/ladder into origin/proxy_v2 2023-11-30 20:10:39 -06:00
Damian Bednarczyk
130fdb6b5b add bing bot + create generic struct for bots 2023-11-30 19:55:34 -06:00
Kevin Pham
0043b67d20 rewrite a href URLs for outline pages 2023-11-30 19:45:51 -06:00
Damian Bednarczyk
ba252f5750 add other headers 2023-11-30 19:24:50 -06:00
Kevin Pham
cd891d88a8 tweak outline template rendering 2023-11-30 19:06:33 -06:00
Damian Bednarczyk
8862b7de8b rough draft of more modular design 2023-11-30 16:05:14 -06:00
Kevin Pham
1ec0d2c09e refactor outline api to become a response modifier 2023-11-30 15:50:02 -06:00
joncrangle
1c810ad8e2 Merge branch 'origin/proxy_v2' of https://github.com/everywall/ladder into origin/proxy_v2 2023-11-30 01:50:34 -05:00
joncrangle
f5458aa1bf Fix link hover in dark mode 2023-11-30 01:48:38 -05:00
Kevin Pham
73cae94e53 handle request and url param types in fetch monkey patch 2023-11-30 00:37:55 -06:00
joncrangle
4acb5f615a Fix error message styling 2023-11-30 01:26:42 -05:00
joncrangle
a66f922f39 Update form 2023-11-30 01:17:16 -05:00
joncrangle
c89ba910f3 Tidy 2023-11-30 01:13:38 -05:00
joncrangle
2beeeca89f Rename ExecuteForAPI to ExecuteForOutline 2023-11-30 01:13:27 -05:00
joncrangle
bc01028d2b Add dark mode 2023-11-30 01:12:38 -05:00
joncrangle
6749f76b0b Begin work on outline page and route 2023-11-30 00:57:32 -05:00
Damian Bednarczyk
dbaf1029c5 woops 2023-11-29 23:30:18 -06:00
Damian Bednarczyk
0122b2f1cf add option to use random ip from googlebot pool 2023-11-29 23:24:32 -06:00
Kevin Pham
97afbaa64d add ascii art 2023-11-29 16:41:24 -06:00
Kevin Pham
23fb0253b3 Merge pull request #52 from dxbednarczyk/origin/proxy_v2
tldr
2023-11-29 15:08:24 -06:00
Damian Bednarczyk
439ec61de9 rename api.ApiError to api.Error 2023-11-29 15:03:44 -06:00
Damian Bednarczyk
382d5f3744 ignore unused params 2023-11-29 15:03:30 -06:00
Damian Bednarczyk
dc558b38fa fix import orders 2023-11-29 15:02:05 -06:00
Damian Bednarczyk
3fdbf3524d rename proxychain.ProxyChainPool to proxychain.Pool 2023-11-29 15:00:35 -06:00
Kevin Pham
53ce3ff297 add /api/outline path 2023-11-29 11:38:28 -06:00
Kevin Pham
909d29230f improve outline api with error handling and proper content-type response 2023-11-29 09:50:02 -06:00
Kevin Pham
e5bd5df2c0 structured article json first draft 2023-11-29 00:33:46 -06:00
Kevin Pham
b7a012d75b add outline test 2023-11-28 20:32:36 -06:00
Kevin Pham
31200cf9e2 replace cycletls with bogdanfinn/tls-client for better tls fingerprint spoofing reliability 2023-11-28 12:33:53 -06:00
Kevin Pham
9d31f7eb59 improve token rewriting 2023-11-28 07:37:28 -06:00
Kevin Pham
214790f905 fix fwd response headers 2023-11-28 00:06:41 -06:00
Kevin Pham
9a8d4ac261 fix cycletls imports 2023-11-27 22:40:34 -06:00
Kevin Pham
0fe02c397d fix srcset handler in html rewriter 2023-11-27 21:36:50 -06:00
Kevin Pham
78e15d8342 fix archivist response modifier 2023-11-27 20:29:03 -06:00
Kevin Pham
81cda1101c improve wayback/archist display / refactor cycletls transport with PR in ext lib 2023-11-27 20:09:03 -06:00
Kevin Pham
7b3aecca7d add TLS fingerprint (ja3) spoofer request modifier 2023-11-27 16:30:13 -06:00
Kevin Pham
2dccc7ca35 add req/resp header forwarding modifiers 2023-11-27 12:45:23 -06:00
Kevin Pham
59cf0317ec improve ua modifier with client side spoofing 2023-11-27 12:00:12 -06:00
Kevin Pham
fb63dba8a0 add response / request modifier API to only fire once 2023-11-26 23:15:00 -06:00
Kevin Pham
a295f2a167 aider: Add public API to add a ResponseModification to fire only once.
# Aider chat conversation:

USER: Modify proxychain.go such that there is a public API to add a ResponseModification to fire only once.

ASSISTANT: To make this change we need to modify `proxychain/proxychain.go` to:

1. Add a new field `onceResultModifications` in the `ProxyChain` struct to store the response modifications that should only fire once.
2. Add a new method `AddOnceResponseModification` to add a response modification that should only fire once.
3. Update the `_execute` method to apply the `onceResultModifications` and clear them after they have been applied.

Here are the *SEARCH/REPLACE* blocks:

proxychain/proxychain.go
<source>
<<<<<<< SEARCH
onceRequestModifications []RequestModification
resultModifications     []ResponseModification
=======
onceRequestModifications []RequestModification
onceResultModifications []ResponseModification
resultModifications     []ResponseModification
>>>>>>> REPLACE
</source>

proxychain/proxychain.go
<source>
<<<<<<< SEARCH
// AddOnceRequestModification adds a request modifier to the ProxyChain that should only fire once
// the modifier will not fire until ProxyChain.Execute() is run and will be removed after it has been applied.
func (chain *ProxyChain) AddOnceRequestModification(mod ...RequestModification) *ProxyChain {
	chain.onceRequestModifications = append(chain.onceRequestModifications, mod...)
	return chain
}
=======
// AddOnceRequestModification adds a request modifier to the ProxyChain that should only fire once
// the modifier will not fire until ProxyChain.Execute() is run and will be removed after it has been applied.
func (chain *ProxyChain) AddOnceRequestModification(mod ...RequestModification) *ProxyChain {
	chain.onceRequestModifications = append(chain.onceRequestModifications, mod...)
	return chain
}

// AddOnceResponseModification adds a response modifier to the ProxyChain that should only fire once
// the modifier will not fire until ProxyChain.Execute() is run and will be removed after it has been applied.
func (chain *ProxyChain) AddOnceResponseModification(mod ...ResponseModification) *ProxyChain {
	chain.onceResultModifications = append(chain.onceResultModifications, mod...)
	return chain
}
>>>>>>> REPLACE
</source>

proxychain/proxychain.go
<source>
<<<<<<< SEARCH
// Apply onceRequestModifications to proxychain and clear them
for _, applyOnceRequestModificationsTo := range chain.onceRequestModifications {
	err := applyOnceRequestModificationsTo(chain)
	if err != nil {
		return nil, chain.abort(err)
	}
}
chain.onceRequestModifications = nil
=======
// Apply onceRequestModifications and onceResultModifications to proxychain and clear them
for _, applyOnceRequestModificationsTo := range chain.onceRequestModifications {
	err := applyOnceRequestModificationsTo(chain)
	if err != nil {
		return nil, chain.abort(err)
	}
}
chain.onceRequestModifications = nil

for _, applyOnceResultModificationsTo := range chain.onceResultModifications {
	err := applyOnceResultModificationsTo(chain)
	if err != nil {
		return nil, chain.abort(err)
	}
}
chain.onceResultModifications = nil
>>>>>>> REPLACE
</source>
2023-11-26 23:14:03 -06:00
Kevin Pham
642736126d aider: Add public API to add a RequestModification to fire only once.
# Aider chat conversation:

USER: Modify proxychain.go such that there is a public API to add a RequestModification to fire only once.

ASSISTANT: To make this change we need to modify `proxychain/proxychain.go` to:

1. Add a new field `onceRequestModifications` in the `ProxyChain` struct to store the request modifications that should only fire once.
2. Add a new method `AddOnceRequestModification` to add a request modification that should only fire once.
3. Update the `_execute` method to apply the `onceRequestModifications` and clear them after they have been applied.

Here are the *SEARCH/REPLACE* blocks:

proxychain/proxychain.go
<source>
<<<<<<< SEARCH
type ProxyChain struct {
	Context              *fiber.Ctx
	Client               *http.Client
	Request              *http.Request
	Response             *http.Response
	requestModifications []RequestModification
	resultModifications  []ResponseModification
	htmlTokenRewriters   []rr.IHTMLTokenRewriter
	Ruleset              *ruleset.RuleSet
	debugMode            bool
	abortErr             error
}
=======
type ProxyChain struct {
	Context                 *fiber.Ctx
	Client                  *http.Client
	Request                 *http.Request
	Response                *http.Response
	requestModifications    []RequestModification
	onceRequestModifications []RequestModification
	resultModifications     []ResponseModification
	htmlTokenRewriters      []rr.IHTMLTokenRewriter
	Ruleset                 *ruleset.RuleSet
	debugMode               bool
	abortErr                error
}
>>>>>>> REPLACE
</source>

proxychain/proxychain.go
<source>
<<<<<<< SEARCH
// AddRequestModifications sets the ProxyChain's request modifers
// the modifier will not fire until ProxyChain.Execute() is run.
func (chain *ProxyChain) AddRequestModifications(mods ...RequestModification) *ProxyChain {
	chain.requestModifications = append(chain.requestModifications, mods...)
	return chain
}
=======
// AddRequestModifications adds more request modifers to the ProxyChain
// the modifier will not fire until ProxyChain.Execute() is run.
func (chain *ProxyChain) AddRequestModifications(mods ...RequestModification) *ProxyChain {
	chain.requestModifications = append(chain.requestModifications, mods...)
	return chain
}

// AddOnceRequestModification adds a request modifier to the ProxyChain that should only fire once
// the modifier will not fire until ProxyChain.Execute() is run and will be removed after it has been applied.
func (chain *ProxyChain) AddOnceRequestModification(mod ...RequestModification) *ProxyChain {
	chain.onceRequestModifications = append(chain.onceRequestModifications, mod...)
	return chain
}
>>>>>>> REPLACE
</source>

proxychain/proxychain.go
<source>
<<<<<<< SEARCH
// _execute sends the request for the ProxyChain and returns the raw body only
// the caller is responsible for returning a response back to the requestor
// the caller is also responsible for calling chain._reset() when they are done with the body
func (chain *ProxyChain) _execute() (io.Reader, error) {
	if chain.validateCtxIsSet() != nil || chain.abortErr != nil {
		return nil, chain.abortErr
	}
	if chain.Request == nil {
		return nil, errors.New("proxychain request not yet initialized")
	}
	if chain.Request.URL.Scheme == "" {
		return nil, errors.New("request url not set or invalid. Check ProxyChain ReqMods for issues")
	}

	// Apply requestModifications to proxychain
	for _, applyRequestModificationsTo := range chain.requestModifications {
		err := applyRequestModificationsTo(chain)
		if err != nil {
			return nil, chain.abort(err)
		}
	}
=======
// _execute sends the request for the ProxyChain and returns the raw body only
// the caller is responsible for returning a response back to the requestor
// the caller is also responsible for calling chain._reset() when they are done with the body
func (chain *ProxyChain) _execute() (io.Reader, error) {
	if chain.validateCtxIsSet() != nil || chain.abortErr != nil {
		return nil, chain.abortErr
	}
	if chain.Request == nil {
		return nil, errors.New("proxychain request not yet initialized")
	}
	if chain.Request.URL.Scheme == "" {
		return nil, errors.New("request url not set or invalid. Check ProxyChain ReqMods for issues")
	}

	// Apply requestModifications to proxychain
	for _, applyRequestModificationsTo := range chain.requestModifications {
		err := applyRequestModificationsTo(chain)
		if err != nil {
			return nil, chain.abort(err)
		}
	}

	// Apply onceRequestModifications to proxychain and clear them
	for _, applyOnceRequestModificationsTo := range chain.onceRequestModifications {
		err := applyOnceRequestModificationsTo(chain)
		if err != nil {
			return nil, chain.abort(err)
		}
	}
	chain.onceRequestModifications = nil
>>>>>>> REPLACE
</source>
2023-11-26 23:12:27 -06:00
Kevin Pham
98d6b65057 simplify rewriters api usage 2023-11-26 22:51:00 -06:00
Kevin Pham
ae48429da7 merge main 2023-11-26 21:55:17 -06:00
Kevin Pham
cb409f96d4 add package.json 2023-11-26 17:48:11 -06:00
Kevin Pham
d71ebe5137 test demo 2023-11-26 16:44:04 -06:00
ladddder
8f447eab2e Merge pull request #47 from dxbednarczyk/main
Idiomatize(?) ruleset package and run lint
2023-11-25 22:25:20 +01:00
Damian
dc19c4c813 output to stdout by default 2023-11-24 21:25:12 +00:00
mms-gianni
37fad659a2 Merge pull request #48 from joncrangle/feat/air
Add air live reload
2023-11-23 17:50:38 +01:00
joncrangle
6f28773750 Update dev instructions for air 2023-11-23 09:33:08 -05:00
Kevin Pham
6c54d31086 add dynamic resource url patcher as standalone responsemodifier 2023-11-23 08:14:52 -06:00
Kevin Pham
5d55a2f3f0 refactor rewriters to modify html in single pass with multiple rewriters; improve html rewriter edge case handling 2023-11-22 23:51:52 -06:00
joncrangle
b32c1efd45 Add yaml to include_ext 2023-11-22 23:49:43 -05:00
joncrangle
11bb05c8b4 Add air config file 2023-11-22 22:50:37 -05:00
Damian Bednarczyk
dc69af9f38 idiomatize (?) ruleset package and lint 2023-11-22 21:26:44 -06:00
Kevin Pham
7668713b1a fix recursive proxy calls 2023-11-22 07:21:48 -06:00
Kevin Pham
bfd647e526 url rewrite improvements 2023-11-21 23:25:34 -06:00
Kevin Pham
efa43a6f36 minor fix with parameterization 2023-11-21 20:58:16 -06:00
Kevin Pham
854dafbcfa improve js rewriting functionality by not relying on js to get proxy and proxified URLs; direct injection from golang 2023-11-21 20:54:09 -06:00
Kevin Pham
a4e016b36c add common referrer options 2023-11-21 20:33:52 -06:00
Kevin Pham
0e620e46ab organize rewriters 2023-11-21 18:44:33 -06:00
Kevin Pham
0fc0942095 encorporate url encoding issue fix from ddba232a31 2023-11-21 15:09:24 -06:00
Kevin Pham
dab77d786f url rewriter tweaks 2023-11-21 14:10:37 -06:00
Kevin Pham
543192afbe support js URL rewriting; support post req 2023-11-21 10:45:29 -06:00
Kevin Pham
79a229f28c handle srcset resource URL rewrites; monkey patch JS for URL rewrites 2023-11-20 23:42:50 -06:00
Kevin Pham
6222476684 forward content-type headers 2023-11-20 15:49:39 -06:00
Kevin Pham
5d46adc486 wip 2023-11-20 15:37:07 -06:00
Kevin Pham
1d88f14de2 rewrite resource URLs based on html tokenizer instead of regex 2023-11-20 11:38:53 -06:00
ladddder
394eaf9805 Merge pull request #44 from everywall/docs/add-how-it-works
add how it works
2023-11-20 16:54:22 +01:00
ladddder
24ad760119 add how it works 2023-11-20 16:45:13 +01:00
Kevin Pham
5035f65d6b wip 2023-11-19 20:59:55 -06:00
Kevin Pham
ee9066dedb refactor wip 2023-11-19 15:03:11 -06:00
Kevin Pham
98fa53287b fix nil pointer deref 2023-11-18 18:13:46 -06:00
Kevin Pham
f6341f2c3e begin refactor of proxy engine 2023-11-18 08:31:59 -06:00
Gianni Carafa
6d8e943df5 add env var docker run command 2023-11-16 14:14:17 +01:00
Gianni Carafa
68e5023ed9 Revert "remove rulesets from base repository"
This reverts commit 8d00e29c43.
2023-11-16 14:01:57 +01:00
Gianni Carafa
8d00e29c43 remove rulesets from base repository 2023-11-16 13:30:23 +01:00
Gianni Carafa
c8d39ea21f readd ruleset 2023-11-16 13:27:57 +01:00
Gianni Carafa
dae4afb55e fix typo 2023-11-16 13:10:55 +01:00
mms-gianni
a83503170e Merge pull request #41 from deoxykev/refactor_rulesets
refactor rulesets into separate files and add a ruleset compiler cli …
2023-11-16 13:07:11 +01:00
Kevin Pham
0eef3e5808 refactor rulesets into separate files and add a ruleset compiler cli flag 2023-11-15 15:30:23 -06:00
Gianni Carafa
7597ea2807 udpate README 2023-11-15 21:28:23 +01:00
Gianni Carafa
235dca8dd0 minor ruleset improvements 2023-11-15 21:04:42 +01:00
mms-gianni
191279c00c Merge pull request #40 from everywall/39-request-header-fields-too-large
fix request header fields to large
2023-11-15 20:46:09 +01:00
mms-gianni
f4060c3e78 Merge branch 'main' into 39-request-header-fields-too-large 2023-11-15 20:45:59 +01:00
mms-gianni
55284f0b24 Merge pull request #37 from deoxykev/organized_rulesets
Organized rulesets
2023-11-15 20:45:10 +01:00
mms-gianni
f7f4586032 Merge branch 'main' into organized_rulesets 2023-11-15 20:40:36 +01:00
Gianni Carafa
fe881ca661 use cookie method to empty cookie header 2023-11-15 16:48:00 +01:00
Gianni Carafa
86700d8828 set empty cookie 2023-11-15 16:34:56 +01:00
Kevin Pham
a8d920548c add feature to load ruleset from directory or gzip file on http server, refactor ruleset loading logic 2023-11-14 15:57:39 -06:00
Kevin Pham
e87d19d7f5 add ability to load rulesets from directory 2023-11-14 15:42:26 -06:00
140 changed files with 10345 additions and 994 deletions

46
.air.toml Normal file
View File

@@ -0,0 +1,46 @@
root = "./"
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata",]
exclude_file = ["proxychain/ruleset/rule_resmod_types.gen.go", "proxychain/ruleset/rule_reqmod_types.gen.go", "handlers/api_modifiers_structdef.gen.go"]
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = "./tmp/main --ruleset ./rulesets_v2"
include_dir = []
include_ext = ["go", "tpl", "tmpl", "yaml", "html", "js"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = ["git submodule update --init --recursive; git rev-parse --short HEAD > handlers/VERSION; git rev-parse --short HEAD > cmd/VERSION; cd proxychain/codegen && go run codegen.go && cd ../../handlers/api_modifiers_codegen && go run api_modifiers_codegen.go"]
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = true
[screen]
clear_on_rebuild = true
keep_scroll = true

View File

@@ -3,7 +3,10 @@ name: Build Tailwind CSS
on: on:
push: push:
paths: paths:
- "handlers/error_page.html"
- "handlers/form.html" - "handlers/form.html"
- "handlers/playground.html"
- "proxychain/responsemodifiers/vendor/generate_readable_outline.html"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -27,16 +30,17 @@ jobs:
name: Build Tailwind CSS name: Build Tailwind CSS
run: pnpm build run: pnpm build
- -
name: Commit generated stylesheet name: Commit generated stylesheet for handlers/styles.css
run: | run: |
if git diff --quiet cmd/styles.css; then if git diff --quiet handlers/styles.css; then
echo "No changes to commit." echo "No changes to commit."
exit 0 exit 0
else else
echo "Changes detected, committing..." echo "Changes detected, committing..."
git config --global user.name "Github action" git config --global user.name "Github action"
git config --global user.email "username@users.noreply.github.com" git config --global user.email "username@users.noreply.github.com"
git add cmd git add handlers
git add proxychain/responsemodifiers/vendor/
git commit -m "Generated stylesheet" git commit -m "Generated stylesheet"
git push git push
fi fi

View File

@@ -22,11 +22,7 @@ jobs:
- -
name: Set version name: Set version
run: | run: |
VERSION=$(git describe --tags --abbrev=0) echo -n $(git describe --tags --abbrev=0) > handlers/VERSION
echo -n $VERSION > handlers/VERSION
sed -i 's\VERSION\${VERSION}\g' handlers/form.html
echo handlers/form.html >> .gitignore
echo .gitignore >> .gitignore
- -
name: Set up Go name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3

View File

@@ -42,11 +42,7 @@ jobs:
- name: Set version - name: Set version
id: version id: version
run: | run: |
VERSION=$(git describe --tags --abbrev=0) echo ${GITHUB_REF#refs/tags/v} > handlers/VERSION
echo -n $VERSION > handlers/VERSION
sed -i 's\VERSION\${VERSION}\g' handlers/form.html
echo handlers/form.html >> .gitignore
echo .gitignore >> .gitignore
# Install the cosign tool except on PR # Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer # https://github.com/sigstore/cosign-installer

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
# dev binary # dev binary
ladder ladder
tmp/main
tmp
VERSION VERSION
output.css output.css
.aider*

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "proxychain/responsemodifiers/vendor/ddg-tracker-surrogates"]
path = proxychain/responsemodifiers/vendor/ddg-tracker-surrogates
url = https://github.com/duckduckgo/tracker-surrogates
[submodule "proxychain/requestmodifiers/vendor/ua-parser-js"]
path = proxychain/requestmodifiers/vendor/ua-parser-js
url = https://github.com/faisalman/ua-parser-js.git

View File

@@ -7,7 +7,7 @@ COPY . .
RUN go mod download RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o ladder cmd/main.go RUN make build
FROM debian:12-slim as release FROM debian:12-slim as release
@@ -18,8 +18,4 @@ RUN chmod +x /app/ladder
RUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/* RUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*
#EXPOSE 8080
#ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["sh", "-c", "/app/ladder"] CMD ["sh", "-c", "/app/ladder"]

View File

@@ -1,6 +1,14 @@
build:
cd proxychain/codegen && go run codegen.go
cd handlers/api_modifiers_codegen && go run api_modifiers_codegen.go
git submodule update --init --recursive
git rev-parse --short HEAD > handlers/VERSION
git rev-parse --short HEAD > cmd/VERSION
go build -o ladder -ldflags="-s -w" cmd/main.go
lint: lint:
gofumpt -l -w . gofumpt -l -w .
golangci-lint run -c .golangci-lint.yaml golangci-lint run -c .golangci-lint.yaml --fix
go mod tidy go mod tidy
go clean go clean
@@ -8,3 +16,6 @@ lint:
install-linters: install-linters:
go install mvdan.cc/gofumpt@latest go install mvdan.cc/gofumpt@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
run:
go run ./cmd/.

View File

@@ -14,6 +14,18 @@ Freedom of information is an essential pillar of democracy and informed decision
> **Disclaimer:** This project is intended for educational purposes only. The author does not endorse or encourage any unethical or illegal activity. Use this tool at your own risk. > **Disclaimer:** This project is intended for educational purposes only. The author does not endorse or encourage any unethical or illegal activity. Use this tool at your own risk.
### How it works
```mermaid
sequenceDiagram
client->>+ladder: GET
ladder-->>ladder: apply RequestModifications
ladder->>+website: GET
website->>-ladder: 200 OK
ladder-->>ladder: apply ResultModifications
ladder->>-client: 200 OK
```
### Features ### Features
- [x] Bypass Paywalls - [x] Bypass Paywalls
- [x] Remove CORS headers from responses, assets, and images ... - [x] Remove CORS headers from responses, assets, and images ...
@@ -48,12 +60,12 @@ Certain sites may display missing images or encounter formatting issues. This ca
### Binary ### Binary
1) Download binary [here](https://github.com/everywall/ladder/releases/latest) 1) Download binary [here](https://github.com/everywall/ladder/releases/latest)
2) Unpack and run the binary `./ladder` 2) Unpack and run the binary `./ladder -r https://t.ly/14PSf`
3) Open Browser (Default: http://localhost:8080) 3) Open Browser (Default: http://localhost:8080)
### Docker ### Docker
```bash ```bash
docker run -p 8080:8080 -d --name ladder ghcr.io/everywall/ladder:latest docker run -p 8080:8080 -d --env RULESET=https://t.ly/14PSf --name ladder ghcr.io/everywall/ladder:latest
``` ```
### Docker Compose ### Docker Compose
@@ -79,18 +91,28 @@ Or create a bookmark with the following URL:
```javascript ```javascript
javascript:window.location.href="http://localhost:8080/"+location.href javascript:window.location.href="http://localhost:8080/"+location.href
``` ```
### Outline
```bash
curl -X GET "http://localhost:8080/outline/https://www.example.com"
```
### API ### API
```bash ```bash
curl -X GET "http://localhost:8080/api/https://www.example.com" curl -X GET "http://localhost:8080/api/content/https://www.example.com"
``` ```
### RAW ### RAW
http://localhost:8080/raw/https://www.example.com http://localhost:8080/api/raw/https://www.example.com
### Running Ruleset ### Running Ruleset
http://localhost:8080/ruleset http://localhost:8080/api/ruleset
### Running Rule
http://localhost:8080/api/ruleset/https://example.com
### List available modifiers
http://localhost:8080/api/modifiers
## Configuration ## Configuration
@@ -106,7 +128,7 @@ http://localhost:8080/ruleset
| `LOG_URLS` | Log fetched URL's | `true` | | `LOG_URLS` | Log fetched URL's | `true` |
| `DISABLE_FORM` | Disables URL Form Frontpage | `false` | | `DISABLE_FORM` | Disables URL Form Frontpage | `false` |
| `FORM_PATH` | Path to custom Form HTML | `` | | `FORM_PATH` | Path to custom Form HTML | `` |
| `RULESET` | URL to a ruleset file | `https://raw.githubusercontent.com/everywall/ladder/main/ruleset.yaml` or `/path/to/my/rules.yaml` | | `RULESET` | Path or URL to a ruleset file, accepts local directories | `https://raw.githubusercontent.com/everywall/ladder-rules/main/ruleset.yaml` or `/path/to/my/rules.yaml` or `/path/to/my/rules/` |
| `EXPOSE_RULESET` | Make your Ruleset available to other ladders | `true` | | `EXPOSE_RULESET` | Make your Ruleset available to other ladders | `true` |
| `ALLOWED_DOMAINS` | Comma separated list of allowed domains. Empty = no limitations | `` | | `ALLOWED_DOMAINS` | Comma separated list of allowed domains. Empty = no limitations | `` |
| `ALLOWED_DOMAINS_RULESET` | Allow Domains from Ruleset. false = no limitations | `false` | | `ALLOWED_DOMAINS_RULESET` | Allow Domains from Ruleset. false = no limitations | `false` |
@@ -115,9 +137,10 @@ http://localhost:8080/ruleset
### Ruleset ### Ruleset
It is possible to apply custom rules to modify the response or the requested URL. This can be used to remove unwanted or modify elements from the page. The ruleset is a YAML file that contains a list of rules for each domain and is loaded on startup It is possible to apply custom rules to modify the response or the requested URL. This can be used to remove unwanted or modify elements from the page. The ruleset is a YAML file, a directory with YAML Files, or an URL to a YAML file that contains a list of rules for each domain. These rules are loaded on startup.
There is a basic ruleset available in a separate repository [ruleset.yaml](https://raw.githubusercontent.com/everywall/ladder-rules/main/ruleset.yaml). Feel free to add your own rules and create a pull request.
See in [ruleset.yaml](ruleset.yaml) for an example.
```yaml ```yaml
- domain: example.com # Includes all subdomains - domain: example.com # Includes all subdomains
@@ -176,8 +199,21 @@ See in [ruleset.yaml](ruleset.yaml) for an example.
To run a development server at http://localhost:8080: To run a development server at http://localhost:8080:
```bash ```bash
echo "DEV" > handler/VERSION git clone git@github.com-ladddder:everywall/ladder.git
git submodule update --init --recursive
echo "dev " > handlers/VERSION
echo "dev " > cmd/VERSION
RULESET="./ruleset.yaml" go run cmd/main.go RULESET="./ruleset.yaml" go run cmd/main.go
``` ```
### Optional: Live reloading development server with [cosmtrek/air](https://github.com/cosmtrek/air)
Install air according to the [installation instructions](https://github.com/cosmtrek/air#installation).
Run a development server at http://localhost:8080:
```bash
air # or the path to air if you haven't added a path alias to your .bashrc or .zshrc
```
This project uses [pnpm](https://pnpm.io/) to build a stylesheet with the [Tailwind CSS](https://tailwindcss.com/) classes. For local development, if you modify styles in `form.html`, run `pnpm build` to generate a new stylesheet. This project uses [pnpm](https://pnpm.io/) to build a stylesheet with the [Tailwind CSS](https://tailwindcss.com/) classes. For local development, if you modify styles in `form.html`, run `pnpm build` to generate a new stylesheet.

View File

@@ -1,24 +1,23 @@
package main package main
import ( import (
"embed" _ "embed"
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"ladder/handlers" "github.com/everywall/ladder/handlers"
"github.com/everywall/ladder/internal/cli"
"github.com/everywall/ladder/proxychain/requestmodifiers/bot"
ruleset_v2 "github.com/everywall/ladder/proxychain/ruleset"
"github.com/akamensky/argparse" "github.com/akamensky/argparse"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/favicon"
) )
//go:embed favicon.ico //go:embed VERSION
var faviconData string var version string
//go:embed styles.css
var cssData embed.FS
func main() { func main() {
parser := argparse.NewParser("ladder", "Every Wall needs a Ladder") parser := argparse.NewParser("ladder", "Every Wall needs a Ladder")
@@ -27,6 +26,7 @@ func main() {
if os.Getenv("PORT") == "" { if os.Getenv("PORT") == "" {
portEnv = "8080" portEnv = "8080"
} }
port := parser.String("p", "port", &argparse.Options{ port := parser.String("p", "port", &argparse.Options{
Required: false, Required: false,
Default: portEnv, Default: portEnv,
@@ -38,58 +38,139 @@ func main() {
Help: "This will spawn multiple processes listening", Help: "This will spawn multiple processes listening",
}) })
verbose := parser.Flag("v", "verbose", &argparse.Options{
Required: false,
Help: "Adds verbose logging",
})
randomGoogleBot := parser.Flag("", "random-googlebot", &argparse.Options{
Required: false,
Help: "Update the list of trusted Googlebot IPs, and use a random one for each masqueraded request",
})
randomBingBot := parser.Flag("", "random-bingbot", &argparse.Options{
Required: false,
Help: "Update the list of trusted Bingbot IPs, and use a random one for each masqueraded request",
})
// TODO: add version flag that reads from handers/VERSION
ruleset := parser.String("r", "ruleset", &argparse.Options{
Required: false,
Help: "File, Directory or URL to a ruleset.yaml. Overrides RULESET environment variable.",
})
mergeRulesets := parser.Flag("", "merge-rulesets", &argparse.Options{
Required: false,
Help: "Compiles a directory of yaml files into a single ruleset.yaml. Requires --ruleset arg.",
})
mergeRulesetsOutput := parser.String("", "merge-rulesets-output", &argparse.Options{
Required: false,
Help: "Specify output file for --merge-rulesets. Requires --ruleset and --merge-rulesets args.",
})
err := parser.Parse(os.Args) err := parser.Parse(os.Args)
if err != nil { if err != nil {
fmt.Print(parser.Usage(err)) fmt.Print(parser.Usage(err))
} }
if *randomGoogleBot {
err := bot.GoogleBot.UpdatePool("https://developers.google.com/static/search/apis/ipranges/googlebot.json")
if err != nil {
fmt.Println("error while retrieving list of Googlebot IPs: " + err.Error())
fmt.Println("defaulting to known trusted Googlebot identity")
}
}
if *randomBingBot {
err := bot.BingBot.UpdatePool("https://www.bing.com/toolbox/bingbot.json")
if err != nil {
fmt.Println("error while retrieving list of Bingbot IPs: " + err.Error())
fmt.Println("defaulting to known trusted Bingbot identity")
}
}
// utility cli flag to compile ruleset directory into single ruleset.yaml
if *mergeRulesets {
output := os.Stdout
if *mergeRulesetsOutput != "" {
output, err = os.Create(*mergeRulesetsOutput)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
err = cli.HandleRulesetMerge(*ruleset, *mergeRulesets, output)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(0)
}
if os.Getenv("PREFORK") == "true" { if os.Getenv("PREFORK") == "true" {
*prefork = true *prefork = true
} }
var rs ruleset_v2.IRuleset
switch {
case *ruleset != "":
rs, err = ruleset_v2.NewRuleset(*ruleset)
if err != nil {
fmt.Printf("ERROR: failed to load ruleset from %s\n", *ruleset)
}
case os.Getenv("RULESET") != "":
rs = ruleset_v2.NewRulesetFromEnv()
}
app := fiber.New( app := fiber.New(
fiber.Config{ fiber.Config{
Prefork: *prefork, Prefork: *prefork,
GETOnly: true, GETOnly: false,
ReadBufferSize: 4096 * 4, // increase max header size
DisableStartupMessage: true,
}, },
) )
userpass := os.Getenv("USERPASS") app.Use(handlers.Auth())
if userpass != "" { app.Use(handlers.Favicon())
userpass := strings.Split(userpass, ":") app.Use(handlers.RenderErrorPage())
app.Use(basicauth.New(basicauth.Config{
Users: map[string]string{
userpass[0]: userpass[1],
},
}))
}
app.Use(favicon.New(favicon.Config{
Data: []byte(faviconData),
URL: "/favicon.ico",
}))
if os.Getenv("NOLOGS") != "true" { if os.Getenv("NOLOGS") != "true" {
app.Use(func(c *fiber.Ctx) error { app.Use(func(c *fiber.Ctx) error {
log.Println(c.Method(), c.Path()) log.Println(c.Method(), c.Path())
return c.Next() return c.Next()
}) })
} }
app.Get("/", handlers.Form) proxyOpts := &handlers.ProxyOptions{
app.Get("/styles.css", func(c *fiber.Ctx) error { Verbose: *verbose,
cssData, err := cssData.ReadFile("styles.css") Ruleset: rs,
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
} }
c.Set("Content-Type", "text/css")
return c.Send(cssData)
})
app.Get("ruleset", handlers.Ruleset)
app.Get("raw/*", handlers.Raw) app.Get("/", handlers.Form)
app.Get("api/*", handlers.Api)
app.Get("/*", handlers.ProxySite)
app.Get("styles.css", handlers.Styles)
app.Get("script.js", handlers.Script)
app.Get("playground-script.js", handlers.Script)
app.All("api/raw/*", handlers.NewRawProxySiteHandler(proxyOpts))
app.Get("api/modifiers", handlers.NewAPIModifersListHandler(proxyOpts))
app.Get("api/ruleset/*", handlers.NewRulesetSiteHandler(proxyOpts))
app.Get("api/content/*", handlers.NewAPIContentHandler("api/outline/*", proxyOpts))
app.Get("outline/*", handlers.NewOutlineHandler("outline/*", proxyOpts))
app.All("playground/*", handlers.PlaygroundHandler("playground/*", proxyOpts))
app.All("/*", handlers.NewProxySiteHandler(proxyOpts))
fmt.Println(cli.StartupMessage(version, *port, *ruleset))
log.Fatal(app.Listen(":" + *port)) log.Fatal(app.Listen(":" + *port))
} }

File diff suppressed because one or more lines are too long

View File

@@ -3,16 +3,17 @@ services:
ladder: ladder:
image: ghcr.io/everywall/ladder:latest image: ghcr.io/everywall/ladder:latest
container_name: ladder container_name: ladder
#build: . build: .
#restart: always #restart: always
#command: sh -c ./ladder #command: sh -c ./ladder
environment: environment:
- PORT=8080 - PORT=8080
- RULESET=/app/ruleset.yaml - RULESET=/app/ruleset.yaml
#- ALLOWED_DOMAINS=example.com,example.org
#- ALLOWED_DOMAINS_RULESET=false #- ALLOWED_DOMAINS_RULESET=false
#- EXPOSE_RULESET=true #- EXPOSE_RULESET=true
#- PREFORK=false #- PREFORK=false
#- DISABLE_FORM=fase #- DISABLE_FORM=false
#- FORM_PATH=/app/form.html #- FORM_PATH=/app/form.html
#- X_FORWARDED_FOR=66.249.66.1 #- X_FORWARDED_FOR=66.249.66.1
#- USER_AGENT=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) #- USER_AGENT=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)

44
go.mod
View File

@@ -1,29 +1,59 @@
module ladder module github.com/everywall/ladder
go 1.21.1 go 1.21.1
require ( require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/akamensky/argparse v1.4.0 github.com/akamensky/argparse v1.4.0
github.com/gofiber/fiber/v2 v2.50.0 github.com/bogdanfinn/fhttp v0.5.24
github.com/bogdanfinn/tls-client v1.6.1
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c
github.com/gofiber/fiber/v2 v2.51.0
github.com/markusmobius/go-trafilatura v1.5.1
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
golang.org/x/net v0.19.0
golang.org/x/term v0.15.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/abadojack/whatlanggo v1.0.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bogdanfinn/utls v1.5.16 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elliotchance/pie/v2 v2.8.0 // indirect
github.com/forPelevin/gomoji v1.1.8 // indirect
github.com/go-shiori/go-readability v0.0.0-20231029095239-6b97d5aba789 // indirect
github.com/gofiber/template v1.8.2 // indirect
github.com/gofiber/template/html/v2 v2.0.5
github.com/gofiber/utils v1.1.0 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.4.0 // indirect github.com/google/uuid v1.4.0 // indirect
github.com/klauspost/compress v1.17.2 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/markusmobius/go-dateparser v1.2.1 // indirect
github.com/markusmobius/go-domdistiller v0.0.0-20230515154422-71af71939ff3 // indirect
github.com/markusmobius/go-htmldate v1.2.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
github.com/tetratelabs/wazero v1.5.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/net v0.18.0 // indirect github.com/wasilibs/go-re2 v1.4.1 // indirect
golang.org/x/sys v0.14.0 // indirect github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
) )

109
go.sum
View File

@@ -1,85 +1,158 @@
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/bogdanfinn/fhttp v0.5.24 h1:OlyBKjvJp6a3TotN3wuj4mQHHRbfK7QUMrzCPOZGhRc=
github.com/bogdanfinn/fhttp v0.5.24/go.mod h1:brqi5woc5eSCVHdKYBV8aZLbO7HGqpwyDLeXW+fT18I=
github.com/bogdanfinn/tls-client v1.6.1 h1:GTIqQssFoIvLaDf4btoYRzDhUzudLqYD4axvfUCXl3I=
github.com/bogdanfinn/tls-client v1.6.1/go.mod h1:FtwQ3DndVZ0xAOO704v4iNAgbHOcEc5kPk9tjICTNQ0=
github.com/bogdanfinn/utls v1.5.16 h1:NhhWkegEcYETBMj9nvgO4lwvc6NcLH+znrXzO3gnw4M=
github.com/bogdanfinn/utls v1.5.16/go.mod h1:mHeRCi69cUiEyVBkKONB1cAbLjRcZnlJbGzttmiuK4o=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= github.com/elliotchance/pie/v2 v2.8.0 h1://QS43W8sEha8XV/fjngO5iMudN3XARJV5cpBayAcVY=
github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= github.com/elliotchance/pie/v2 v2.8.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
github.com/forPelevin/gomoji v1.1.8 h1:JElzDdt0TyiUlecy6PfITDL6eGvIaxqYH1V52zrd0qQ=
github.com/forPelevin/gomoji v1.1.8/go.mod h1:8+Z3KNGkdslmeGZBC3tCrwMrcPy5GRzAD+gL9NAwMXg=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c h1:wpkoddUomPfHiOziHZixGO5ZBS73cKqVzZipfrLmO1w=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c/go.mod h1:oVDCh3qjJMLVUSILBRwrm+Bc6RNXGZYtoh9xdvf1ffM=
github.com/go-shiori/go-readability v0.0.0-20231029095239-6b97d5aba789 h1:G6wSuUyCoLB9jrUokipsmFuRi8aJozt3phw/g9Sl4Xs=
github.com/go-shiori/go-readability v0.0.0-20231029095239-6b97d5aba789/go.mod h1:2DpZlTJO/ycxp/vsc/C11oUyveStOgIXB88SYV1lncI=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.0.5 h1:BKLJ6Qr940NjntbGmpO3zVa4nFNGDCi/IfUiDB9OC20=
github.com/gofiber/template/html/v2 v2.0.5/go.mod h1:RCF14eLeQDCSUPp0IGc2wbSSDv6yt+V54XB/+Unz+LM=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/markusmobius/go-dateparser v1.2.1 h1:mYRRdu3TzpAeE6fSl2Gn3arfxEtoTRvFOKlumlVsUtg=
github.com/markusmobius/go-dateparser v1.2.1/go.mod h1:5xYsZ1h7iB3sE1BSu8bkjYpbFST7EU1/AFxcyO3mgYg=
github.com/markusmobius/go-domdistiller v0.0.0-20230515154422-71af71939ff3 h1:D83RvMz1lQ0ilKlJt6DWc65+Q77CXGRFmfihR0bfQvc=
github.com/markusmobius/go-domdistiller v0.0.0-20230515154422-71af71939ff3/go.mod h1:n1AYw0wiJDT3YXnIsElJPiDR63YGXT2yv3uq0CboGmU=
github.com/markusmobius/go-htmldate v1.2.2 h1:tp1IxhefCYpEoL9CM1LiU6l+2YayTpuTjkkdnik6hXE=
github.com/markusmobius/go-htmldate v1.2.2/go.mod h1:26VRz16sCosuiv42MNRW9iPBGnGLo+q/Z6TWitt8uzs=
github.com/markusmobius/go-trafilatura v1.5.1 h1:EXhZY2AVRyepUlLZHeuZUme3v7Ms9G8lDOLl4u+Jp5M=
github.com/markusmobius/go-trafilatura v1.5.1/go.mod h1:FhuBBPZ9ph4ufpGBKAkuq5oQwEhg0KKnIOUlv5h7EHg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0=
github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/wasilibs/go-re2 v1.4.1 h1:E5+9O1M8UoGeqLB2A9omeoaWImqpuYDs9cKwvTJq/Oo=
github.com/wasilibs/go-re2 v1.4.1/go.mod h1:ynB8eCwd9JsqUnsk8WlPDk6cEeme8BguZmnqOSURE4Y=
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,7 +2,6 @@ package handlers
import ( import (
_ "embed" _ "embed"
"log"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -12,48 +11,5 @@ import (
var version string var version string
func Api(c *fiber.Ctx) error { func Api(c *fiber.Ctx) error {
// Get the url from the URL return nil
urlQuery := c.Params("*")
queries := c.Queries()
body, req, resp, err := fetchSite(urlQuery, queries)
if err != nil {
log.Println("ERROR:", err)
c.SendStatus(500)
return c.SendString(err.Error())
}
response := Response{
Version: version,
Body: body,
}
response.Request.Headers = make([]any, 0, len(req.Header))
for k, v := range req.Header {
response.Request.Headers = append(response.Request.Headers, map[string]string{
"key": k,
"value": v[0],
})
}
response.Response.Headers = make([]any, 0, len(resp.Header))
for k, v := range resp.Header {
response.Response.Headers = append(response.Response.Headers, map[string]string{
"key": k,
"value": v[0],
})
}
return c.JSON(response)
}
type Response struct {
Version string `json:"version"`
Body string `json:"body"`
Request struct {
Headers []interface{} `json:"headers"`
} `json:"request"`
Response struct {
Headers []interface{} `json:"headers"`
} `json:"response"`
} }

View File

@@ -1,44 +0,0 @@
// BEGIN: 7d5e1f7c7d5e
package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
func TestApi(t *testing.T) {
app := fiber.New()
app.Get("/api/*", Api)
tests := []struct {
name string
url string
expectedStatus int
}{
{
name: "valid url",
url: "https://www.google.com",
expectedStatus: http.StatusOK,
},
{
name: "invalid url",
url: "invalid-url",
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/"+tt.url, nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
})
}
}
// END: 7d5e1f7c7d5e

45
handlers/api_content.go Normal file
View File

@@ -0,0 +1,45 @@
package handlers
import (
rx "github.com/everywall/ladder/proxychain/requestmodifiers"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
"github.com/gofiber/fiber/v2"
)
func NewAPIContentHandler(path string, opts *ProxyOptions) fiber.Handler {
// TODO: implement ruleset logic
/*
var rs ruleset.RuleSet
if opts.RulesetPath != "" {
r, err := ruleset.NewRuleset(opts.RulesetPath)
if err != nil {
panic(err)
}
rs = r
}
*/
return func(c *fiber.Ctx) error {
proxychain := proxychain.
NewProxyChain().
WithAPIPath(path).
SetDebugLogging(opts.Verbose).
SetRequestModifications(
rx.MasqueradeAsGoogleBot(),
rx.ForwardRequestHeaders(),
rx.SpoofReferrerFromGoogleSearch(),
).
AddResponseModifications(
tx.DeleteIncomingCookies(),
tx.RewriteHTMLResourceURLs(),
tx.APIContent(),
).
SetFiberCtx(c).
Execute()
return proxychain
}
}

29
handlers/api_modifiers.go Normal file
View File

@@ -0,0 +1,29 @@
package handlers
import (
"encoding/json"
"github.com/everywall/ladder/proxychain/responsemodifiers/api"
"github.com/gofiber/fiber/v2"
)
func NewAPIModifersListHandler(opts *ProxyOptions) fiber.Handler {
payload := ModifiersAPIResponse{
Success: true,
Result: AllMods,
}
body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
panic(err)
}
return func(c *fiber.Ctx) error {
c.Set("content-type", "application/json")
if err != nil {
c.SendStatus(500)
return c.SendStream(api.CreateAPIErrReader(err))
}
return c.Send(body)
}
}

View File

@@ -0,0 +1,196 @@
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/fs"
"os/exec"
//"io/fs"
"os"
"path/filepath"
"strings"
//"strings"
)
func genModStruct(fn *ast.FuncDecl, githubEditLink string, filename string) string {
params := []string{}
for _, fd := range fn.Type.Params.List {
p := fmt.Sprintf(` {Name: "%s", Type: "%+v"},`, fd.Names[0], fd.Type)
params = append(params, p)
}
block := fmt.Sprintf(`{
Name: "%s",
Description: "%s",
CodeEditLink: "%s%s",
Params: []Param{
%s
},
},`,
fn.Name.String(),
strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(fn.Doc.Text()), "\n", " "), `"`, `\"`),
githubEditLink, filename,
strings.Join(params, "\n"),
)
return block
}
func modCodeGen(dir string, githubEditLink string) (code string, err error) {
fset := token.NewFileSet()
files, err := os.ReadDir(dir)
if err != nil {
panic(err)
}
modStructs := []string{}
for _, file := range files {
if !shouldGenCodeFor(file) {
continue
}
// Parse each Go file
node, err := parser.ParseFile(fset, filepath.Join(dir, file.Name()), nil, parser.ParseComments)
if err != nil {
return "", err
}
ast.Inspect(node, func(n ast.Node) bool {
fn, ok := n.(*ast.FuncDecl)
if ok && fn.Recv == nil && fn.Name.IsExported() {
modStructs = append(modStructs, genModStruct(fn, githubEditLink, file.Name()))
}
return true
})
}
code = strings.Join(modStructs, "\n")
return code, nil
}
func shouldGenCodeFor(file fs.DirEntry) bool {
if file.IsDir() {
return false
}
if filepath.Ext(file.Name()) != ".go" {
return false
}
if strings.HasSuffix(file.Name(), "_test.go") {
return false
}
return true
}
func getGitRemoteURL(remoteName string) (string, error) {
cmd := exec.Command("git", "remote", "get-url", remoteName)
output, err := cmd.Output()
if err != nil {
return "", err
}
url := strings.TrimSpace(string(output))
// Convert SSH format to HTTPS format
if strings.HasPrefix(url, "git@") {
url = strings.Replace(url, ":", "/", 1)
url = strings.Replace(url, "git@", "https://", 1)
url = strings.TrimSuffix(url, ".git")
}
return url, nil
}
func getCurrentGitBranch() (string, error) {
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func main() {
gitURL, err := getGitRemoteURL("origin")
if err != nil {
fmt.Println("Error getting Git remote URL:", err)
return
}
branchName, err := getCurrentGitBranch()
if err != nil {
fmt.Println("Error getting current Git branch:", err)
return
}
githubEditLink := fmt.Sprintf("%s/edit/%s/proxychain/requestmodifiers/", gitURL, branchName)
rqmCode, err := modCodeGen("../../proxychain/requestmodifiers/", githubEditLink)
if err != nil {
panic(err)
}
githubEditLink = fmt.Sprintf("%s/edit/%s/proxychain/responsemodifiers/", gitURL, branchName)
rsmCode, err := modCodeGen("../../proxychain/responsemodifiers/", githubEditLink)
if err != nil {
panic(err)
}
code := fmt.Sprintf(`
package handlers
// DO NOT EDIT THIS FILE. It is automatically generated by ladder/handlers/api_modifiers_codegen/api_modifiers_codegen.go
// The purpose of this is to produce an API reponse listing all the available modifier, their parameters and usage instructions.
// for use in proxychains.
import (
"github.com/everywall/ladder/proxychain/responsemodifiers/api"
)
type ModifiersAPIResponse struct {
Success bool ||json:"success"||
Error api.ErrorDetails ||json:"error"||
Result Modifiers ||json:"result"||
}
type Modifiers struct {
RequestModifiers []Modifier ||json:"requestmodifiers"||
ResponseModifiers []Modifier ||json:"responsemodifiers"||
}
type Modifier struct {
Name string ||json:"name"||
Description string ||json:"description"||
CodeEditLink string ||json:"code_edit_link"||
Params []Param ||json:"params"||
}
type Param struct {
Name string ||json:"name"||
Type string ||json:"type"||
}
var AllMods Modifiers = Modifiers{
RequestModifiers: []Modifier{
%s
},
ResponseModifiers: []Modifier{
%s
},
}
`, rqmCode, rsmCode)
code = strings.ReplaceAll(code, "||", "`")
//fmt.Println(code)
fq, err := os.Create("../api_modifiers_structdef.gen.go")
if err != nil {
panic(err)
}
_, err = io.WriteString(fq, code)
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,567 @@
package handlers
// DO NOT EDIT THIS FILE. It is automatically generated by ladder/handlers/api_modifiers_codegen/api_modifiers_codegen.go
// The purpose of this is to produce an API reponse listing all the available modifier, their parameters and usage instructions.
// for use in proxychains.
import (
"github.com/everywall/ladder/proxychain/responsemodifiers/api"
)
type ModifiersAPIResponse struct {
Success bool `json:"success"`
Error api.ErrorDetails `json:"error"`
Result Modifiers `json:"result"`
}
type Modifiers struct {
RequestModifiers []Modifier `json:"requestmodifiers"`
ResponseModifiers []Modifier `json:"responsemodifiers"`
}
type Modifier struct {
Name string `json:"name"`
Description string `json:"description"`
CodeEditLink string `json:"code_edit_link"`
Params []Param `json:"params"`
}
type Param struct {
Name string `json:"name"`
Type string `json:"type"`
}
var AllMods Modifiers = Modifiers{
RequestModifiers: []Modifier{
{
Name: "AddCacheBusterQuery",
Description: "AddCacheBusterQuery modifies query params to add a random parameter key In order to get the upstream network stack to serve a fresh copy of the page.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/add_cache_buster_query.go",
Params: []Param{
},
},
{
Name: "ForwardRequestHeaders",
Description: "ForwardRequestHeaders forwards the requests headers sent from the client to the upstream server",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/forward_request_headers.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsGoogleBot",
Description: "MasqueradeAsGoogleBot modifies user agent and x-forwarded for to appear to be a Google Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsBingBot",
Description: "MasqueradeAsBingBot modifies user agent and x-forwarded for to appear to be a Bing Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsWaybackMachineBot",
Description: "MasqueradeAsWaybackMachineBot modifies user agent and x-forwarded for to appear to be a archive.org (wayback machine) Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsFacebookBot",
Description: "MasqueradeAsFacebookBot modifies user agent and x-forwarded for to appear to be a Facebook Bot (link previews?)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsYandexBot",
Description: "MasqueradeAsYandexBot modifies user agent and x-forwarded for to appear to be a Yandex Spider Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsBaiduBot",
Description: "MasqueradeAsBaiduBot modifies user agent and x-forwarded for to appear to be a Baidu Spider Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsDuckDuckBot",
Description: "MasqueradeAsDuckDuckBot modifies user agent and x-forwarded for to appear to be a DuckDuckGo Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "MasqueradeAsYahooBot",
Description: "MasqueradeAsYahooBot modifies user agent and x-forwarded for to appear to be a Yahoo Bot",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/masquerade_as_trusted_bot.go",
Params: []Param{
},
},
{
Name: "ModifyDomainWithRegex",
Description: "",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_domain_with_regex.go",
Params: []Param{
{Name: "matchRegex", Type: "string"},
{Name: "replacement", Type: "string"},
},
},
{
Name: "SetOutgoingCookie",
Description: "SetOutgoingCookie modifes a specific cookie name by modifying the request cookie headers going to the upstream server. If the cookie name does not already exist, it is created.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_outgoing_cookies.go",
Params: []Param{
{Name: "name", Type: "string"},
{Name: "val", Type: "string"},
},
},
{
Name: "SetOutgoingCookies",
Description: "SetOutgoingCookies modifies a client request's cookie header to a raw Cookie string, overwriting existing cookies",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_outgoing_cookies.go",
Params: []Param{
{Name: "cookies", Type: "string"},
},
},
{
Name: "DeleteOutgoingCookie",
Description: "DeleteOutgoingCookie modifies the http request's cookies header to delete a specific request cookie going to the upstream server. If the cookie does not exist, it does not do anything.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_outgoing_cookies.go",
Params: []Param{
{Name: "name", Type: "string"},
},
},
{
Name: "DeleteOutgoingCookies",
Description: "DeleteOutgoingCookies removes the cookie header entirely, preventing any cookies from reaching the upstream server.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_outgoing_cookies.go",
Params: []Param{
},
},
{
Name: "DeleteOutgoingCookiesExcept",
Description: "DeleteOutGoingCookiesExcept prevents non-whitelisted cookies from being sent from the client to the upstream proxy server. Cookies whose names are in the whitelist are not removed.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_outgoing_cookies.go",
Params: []Param{
{Name: "whitelist", Type: "&{Ellipsis:12476 Elt:string}"},
},
},
{
Name: "ModifyPathWithRegex",
Description: "",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_path_with_regex.go",
Params: []Param{
{Name: "matchRegex", Type: "string"},
{Name: "replacement", Type: "string"},
},
},
{
Name: "ModifyQueryParams",
Description: "ModifyQueryParams replaces query parameter values in URL's query params in a ProxyChain's URL. If the query param key doesn't exist, it is created.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_query_params.go",
Params: []Param{
{Name: "key", Type: "string"},
{Name: "value", Type: "string"},
},
},
{
Name: "SetRequestHeader",
Description: "SetRequestHeader modifies a specific outgoing header This is the header that the upstream server will see.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_request_headers.go",
Params: []Param{
{Name: "name", Type: "string"},
{Name: "val", Type: "string"},
},
},
{
Name: "DeleteRequestHeader",
Description: "DeleteRequestHeader modifies a specific outgoing header This is the header that the upstream server will see.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/modify_request_headers.go",
Params: []Param{
{Name: "name", Type: "string"},
},
},
{
Name: "RequestArchiveIs",
Description: "RequestArchiveIs modifies a ProxyChain's URL to request an archived version from archive.is",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/request_archive_is.go",
Params: []Param{
},
},
{
Name: "RequestGoogleCache",
Description: "RequestGoogleCache modifies a ProxyChain's URL to request its Google Cache version.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/request_google_cache.go",
Params: []Param{
},
},
{
Name: "RequestWaybackMachine",
Description: "RequestWaybackMachine modifies a ProxyChain's URL to request the wayback machine (archive.org) version.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/request_wayback_machine.go",
Params: []Param{
},
},
{
Name: "ResolveWithGoogleDoH",
Description: "ResolveWithGoogleDoH modifies a ProxyChain's client to make the request by resolving the URL using Google's DNS over HTTPs service",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/resolve_with_google_doh.go",
Params: []Param{
},
},
{
Name: "SpoofOrigin",
Description: "SpoofOrigin modifies the origin header if the upstream server returns a Vary header it means you might get a different response if you change this",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_origin.go",
Params: []Param{
{Name: "url", Type: "string"},
},
},
{
Name: "HideOrigin",
Description: "HideOrigin modifies the origin header so that it is the original origin, not the proxy",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_origin.go",
Params: []Param{
},
},
{
Name: "SpoofReferrer",
Description: "SpoofReferrer modifies the referrer header. It is useful if the page can be accessed from a search engine or social media site, but not by browsing the website itself. if url is \"\", then the referrer header is removed.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer.go",
Params: []Param{
{Name: "url", Type: "string"},
},
},
{
Name: "HideReferrer",
Description: "HideReferrer modifies the referrer header so that it is the original referrer, not the proxy",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromBaiduSearch",
Description: "SpoofReferrerFromBaiduSearch modifies the referrer header pretending to be from a BaiduSearch",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_baidu_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromBingSearch",
Description: "SpoofReferrerFromBingSearch modifies the referrer header pretending to be from a bing search site",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_bing_search.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromGoogleSearch",
Description: "SpoofReferrerFromGoogleSearch modifies the referrer header pretending to be from a google search site",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_google_search.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromLinkedInPost",
Description: "SpoofReferrerFromLinkedInPost modifies the referrer header pretending to be from a linkedin post",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_linkedin_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromNaverSearch",
Description: "SpoofReferrerFromNaverSearch modifies the referrer header pretending to be from a Naver search (popular in South Korea)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_naver_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromPinterestPost",
Description: "SpoofReferrerFromPinterestPost modifies the referrer header pretending to be from a pinterest post",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_pinterest_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromQQPost",
Description: "SpoofReferrerFromQQPost modifies the referrer header pretending to be from a QQ post (popular social media in China)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_qq_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromRedditPost",
Description: "SpoofReferrerFromRedditPost modifies the referrer header pretending to be from a reddit post",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_reddit_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromTumblrPost",
Description: "SpoofReferrerFromTumblrPost modifies the referrer header pretending to be from a tumblr post",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_tumblr_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromTwitterPost",
Description: "SpoofReferrerFromTwitterPost modifies the referrer header pretending to be from a twitter post",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_twitter_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromVkontaktePost",
Description: "SpoofReferrerFromVkontaktePost modifies the referrer header pretending to be from a vkontakte post (popular in Russia)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_vkontake_post.go",
Params: []Param{
},
},
{
Name: "SpoofReferrerFromWeiboPost",
Description: "SpoofReferrerFromWeiboPost modifies the referrer header pretending to be from a Weibo post (popular in China)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_referrer_from_weibo_post.go",
Params: []Param{
},
},
{
Name: "SpoofUserAgent",
Description: "SpoofUserAgent modifies the user agent",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_user_agent.go",
Params: []Param{
{Name: "ua", Type: "string"},
},
},
{
Name: "SpoofXForwardedFor",
Description: "SpoofXForwardedFor modifies the X-Forwarded-For header in some cases, a forward proxy may interpret this as the source IP",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/requestmodifiers/spoof_x_forwarded_for.go",
Params: []Param{
{Name: "ip", Type: "string"},
},
},
},
ResponseModifiers: []Modifier{
{
Name: "APIContent",
Description: "APIContent creates an JSON representation of the article and returns it as an API response.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/api_content.go",
Params: []Param{
},
},
{
Name: "BlockElementRemoval",
Description: "BlockElementRemoval prevents paywall javascript from removing a particular element by detecting the removal, then immediately reinserting it. This is useful when a page will return a \"fake\" 404, after flashing the content briefly. If the /outline/ API works, but the regular API doesn't, try this modifier.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/block_element_removal.go",
Params: []Param{
{Name: "cssSelector", Type: "string"},
},
},
{
Name: "BlockThirdPartyScripts",
Description: "BlockThirdPartyScripts rewrites HTML and injects JS to block all third party JS from loading.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/block_third_party_scripts.go",
Params: []Param{
},
},
{
Name: "BypassCORS",
Description: "BypassCORS modifies response headers to prevent the browser from enforcing any CORS restrictions. This should run at the end of the chain.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/bypass_cors.go",
Params: []Param{
},
},
{
Name: "BypassContentSecurityPolicy",
Description: "BypassContentSecurityPolicy modifies response headers to prevent the browser from enforcing any CSP restrictions. This should run at the end of the chain.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/bypass_csp.go",
Params: []Param{
},
},
{
Name: "SetContentSecurityPolicy",
Description: "SetContentSecurityPolicy modifies response headers to a specific CSP",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/bypass_csp.go",
Params: []Param{
{Name: "csp", Type: "string"},
},
},
{
Name: "DeleteLocalStorageData",
Description: "DeleteLocalStorageData deletes localstorage cookies. If the page works once in a fresh incognito window, but fails for subsequent loads, try this response modifier alongside DeleteSessionStorageData and DeleteIncomingCookies",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/delete_localstorage_data.go",
Params: []Param{
},
},
{
Name: "DeleteSessionStorageData",
Description: "DeleteSessionStorageData deletes localstorage cookies. If the page works once in a fresh incognito window, but fails for subsequent loads, try this response modifier alongside DeleteLocalStorageData and DeleteIncomingCookies",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/delete_sessionstorage_data.go",
Params: []Param{
},
},
{
Name: "ForwardResponseHeaders",
Description: "ForwardResponseHeaders forwards the response headers from the upstream server to the client",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/forward_response_headers.go",
Params: []Param{
},
},
{
Name: "GenerateReadableOutline",
Description: "GenerateReadableOutline creates an reader-friendly distilled representation of the article. This is a reliable way of bypassing soft-paywalled articles, where the content is hidden, but still present in the DOM.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/generate_readable_outline.go",
Params: []Param{
},
},
{
Name: "InjectScriptBeforeDOMContentLoaded",
Description: "InjectScriptBeforeDOMContentLoaded modifies HTTP responses to inject a JS before DOM Content is loaded (script tag in head)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/inject_script.go",
Params: []Param{
{Name: "js", Type: "string"},
},
},
{
Name: "InjectScriptAfterDOMContentLoaded",
Description: "InjectScriptAfterDOMContentLoaded modifies HTTP responses to inject a JS after DOM Content is loaded (script tag in head)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/inject_script.go",
Params: []Param{
{Name: "js", Type: "string"},
},
},
{
Name: "InjectScriptAfterDOMIdle",
Description: "InjectScriptAfterDOMIdle modifies HTTP responses to inject a JS after the DOM is idle (ie: js framework loaded)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/inject_script.go",
Params: []Param{
{Name: "js", Type: "string"},
},
},
{
Name: "DeleteIncomingCookies",
Description: "DeleteIncomingCookies prevents ALL cookies from being sent from the proxy server back down to the client.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_incoming_cookies.go",
Params: []Param{
{Name: "_", Type: "&{Ellipsis:18780 Elt:string}"},
},
},
{
Name: "DeleteIncomingCookiesExcept",
Description: "DeleteIncomingCookiesExcept prevents non-whitelisted cookies from being sent from the proxy server to the client. Cookies whose names are in the whitelist are not removed.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_incoming_cookies.go",
Params: []Param{
{Name: "whitelist", Type: "&{Ellipsis:19325 Elt:string}"},
},
},
{
Name: "SetIncomingCookies",
Description: "SetIncomingCookies adds a raw cookie string being sent from the proxy server down to the client",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_incoming_cookies.go",
Params: []Param{
{Name: "cookies", Type: "string"},
},
},
{
Name: "SetIncomingCookie",
Description: "SetIncomingCookie modifies a specific cookie in the response from the proxy server to the client.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_incoming_cookies.go",
Params: []Param{
{Name: "name", Type: "string"},
{Name: "val", Type: "string"},
},
},
{
Name: "ModifyIncomingScriptsWithRegex",
Description: "ModifyIncomingScriptsWithRegex modifies all incoming javascript (application/javascript and inline <script> in text/html) using a regex match and replacement.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_incoming_scripts_with_regex.go",
Params: []Param{
{Name: "matchRegex", Type: "string"},
{Name: "replacement", Type: "string"},
},
},
{
Name: "SetResponseHeader",
Description: "SetResponseHeader modifies response headers from the upstream server",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_response_header.go",
Params: []Param{
{Name: "key", Type: "string"},
{Name: "value", Type: "string"},
},
},
{
Name: "DeleteResponseHeader",
Description: "DeleteResponseHeader removes response headers from the upstream server",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/modify_response_header.go",
Params: []Param{
{Name: "key", Type: "string"},
},
},
{
Name: "PatchDynamicResourceURLs",
Description: "PatchDynamicResourceURLs patches the javascript runtime to rewrite URLs client-side. - This function is designed to allow the proxified page to still be browsible by routing all resource URLs through the proxy. - Native APIs capable of network requests will be hooked and the URLs arguments modified to point to the proxy instead. - fetch('/relative_path') -> fetch('/https://proxiedsite.com/relative_path') - Element.setAttribute('src', \"/assets/img.jpg\") -> Element.setAttribute('src', \"/https://proxiedsite.com/assets/img.jpg\") -> fetch('/https://proxiedsite.com/relative_path')",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/patch_dynamic_resource_urls.go",
Params: []Param{
},
},
{
Name: "PatchTrackerScripts",
Description: "PatchTrackerScripts replaces any request to tracker scripts such as google analytics with a no-op stub that mocks the API structure of the original scripts they replace. Some pages depend on the existence of these structures for proper loading, so this may fix some broken elements. Surrogate script code borrowed from: DuckDuckGo Privacy Essentials browser extension for Firefox, Chrome. (Apache 2.0 license)",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/patch_tracker_scripts.go",
Params: []Param{
},
},
{
Name: "RewriteHTMLResourceURLs",
Description: "RewriteHTMLResourceURLs modifies HTTP responses to rewrite URLs attributes in HTML content (such as src, href) - `<img src='/relative_path'>` -> `<img src='/https://proxiedsite.com/relative_path'>` - This function is designed to allow the proxified page to still be browsible by routing all resource URLs through the proxy.",
CodeEditLink: "https://github.com/everywall/ladder/edit/origin/proxy_v2/proxychain/responsemodifiers/rewrite_http_resource_urls.go",
Params: []Param{
},
},
},
}

51
handlers/api_raw.go Normal file
View File

@@ -0,0 +1,51 @@
package handlers
import (
rx "github.com/everywall/ladder/proxychain/requestmodifiers"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
"github.com/gofiber/fiber/v2"
)
func NewRawProxySiteHandler(opts *ProxyOptions) fiber.Handler {
return func(c *fiber.Ctx) error {
proxychain := proxychain.
NewProxyChain().
SetFiberCtx(c).
SetRequestModifications(
rx.AddCacheBusterQuery(),
rx.MasqueradeAsGoogleBot(),
rx.ForwardRequestHeaders(),
rx.HideOrigin(),
rx.DeleteOutgoingCookies(),
rx.SpoofReferrerFromRedditPost(),
)
// no options passed in, return early
if opts == nil {
// return as plaintext, overriding any rules
proxychain.AddOnceResponseModifications(
tx.SetResponseHeader("content-type", "text/plain; charset=UTF-8"),
)
return proxychain.Execute()
}
// load ruleset
rule, exists := opts.Ruleset.GetRule(proxychain.Request.URL)
if exists {
proxychain.AddOnceRequestModifications(rule.RequestModifications...)
proxychain.AddOnceResponseModifications(rule.ResponseModifications...)
}
// return as plaintext, overriding any rules
proxychain.AddOnceResponseModifications(
tx.SetResponseHeader("content-type", "text/plain; charset=UTF-8"),
)
return proxychain.Execute()
}
}

96
handlers/api_ruleset.go Normal file
View File

@@ -0,0 +1,96 @@
package handlers
import (
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
"gopkg.in/yaml.v3"
"net/url"
"strings"
)
func NewRulesetSiteHandler(opts *ProxyOptions) fiber.Handler {
return func(c *fiber.Ctx) error {
if opts == nil {
c.SendStatus(404)
c.SendString("No ruleset specified. Set the RULESET environment variable or use the --ruleset flag.")
}
// no specific rule requested, return the entire ruleset
if c.Params("*") == "" {
switch c.Get("accept") {
case "application/json":
jsn, err := opts.Ruleset.JSON()
if err != nil {
return err
}
c.Set("content-type", "application/json")
return c.Send([]byte(jsn))
default:
yml, err := opts.Ruleset.YAML()
if err != nil {
return err
}
c.Set("content-type", "text/yaml")
return c.Send([]byte(yml))
}
}
// a specific rule was requested by path /ruleset/https://example.com
// return only that particular rule
reqURL, err := extractURLFromContext(c, "api/ruleset/")
if err != nil {
c.SendStatus(404)
return c.SendString(fmt.Sprintf("A rule that matches '%s' was not found in the ruleset. Possible URL formatting issue.", c.Params("*")))
}
rule, exists := opts.Ruleset.GetRule(reqURL)
if !exists {
c.SendStatus(404)
return c.SendString(fmt.Sprintf("A rule that matches '%s' was not found in the ruleset.", reqURL))
}
switch c.Get("accept") {
case "application/json":
jsn, err := json.MarshalIndent(rule, "", " ")
if err != nil {
return err
}
c.Set("content-type", "application/json")
return c.Send(jsn)
default:
yml, err := yaml.Marshal(rule)
if err != nil {
return err
}
c.Set("content-type", "text/yaml")
return c.Send(yml)
}
}
}
// extractURLFromContext extracts a URL from the request ctx.
func extractURLFromContext(ctx *fiber.Ctx, apiPrefix string) (*url.URL, error) {
reqURL := ctx.Params("*")
reqURL = strings.TrimPrefix(reqURL, apiPrefix)
if !strings.HasPrefix(reqURL, "http") {
reqURL = "https://" + reqURL
}
// sometimes client requests doubleroot '//'
// there is a bug somewhere else, but this is a workaround until we find it
if strings.HasPrefix(reqURL, "/") || strings.HasPrefix(reqURL, `%2F`) {
reqURL = strings.TrimPrefix(reqURL, "/")
reqURL = strings.TrimPrefix(reqURL, `%2F`)
}
// unescape url query
uReqURL, err := url.QueryUnescape(reqURL)
if err == nil {
reqURL = uReqURL
}
return url.Parse(reqURL)
}

25
handlers/auth.go Normal file
View File

@@ -0,0 +1,25 @@
package handlers
import (
"os"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
)
func Auth() fiber.Handler {
userpass := os.Getenv("USERPASS")
if userpass != "" {
userpass := strings.Split(userpass, ":")
return basicauth.New(basicauth.Config{
Users: map[string]string{
userpass[0]: userpass[1],
},
})
}
return func(c *fiber.Ctx) error {
return c.Next()
}
}

64
handlers/error_page.go Normal file
View File

@@ -0,0 +1,64 @@
package handlers
import (
"embed"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"strings"
"github.com/everywall/ladder/proxychain/responsemodifiers/api"
"github.com/gofiber/fiber/v2"
)
//go:embed error_page.html
var errorHTML embed.FS
func RenderErrorPage() fiber.Handler {
f := "error_page.html"
tmpl, err := template.ParseFS(errorHTML, f)
if err != nil {
panic(fmt.Errorf("RenderErrorPage Error: %s not found", f))
}
return func(c *fiber.Ctx) error {
if err := c.Next(); err != nil {
c.Response().SetStatusCode(500)
errReader := api.CreateAPIErrReader(err)
if strings.HasPrefix(c.Path(), "/api/") {
c.Set("Content-Type", "application/json")
return c.SendStream(errReader)
}
errMessageBytes, err := io.ReadAll(errReader)
if err != nil {
return err
}
var errMsg api.Error
if err := json.Unmarshal(errMessageBytes, &errMsg); err != nil {
return err
}
if strings.Contains(c.Get("Accept"), "text/plain") {
c.Set("Content-Type", "text/plain")
return c.SendString(errMsg.Error.Message)
}
if strings.Contains(c.Get("Accept"), "text/html") {
c.Set("Content-Type", "text/html")
tmpl.Execute(c.Response().BodyWriter(), fiber.Map{
"Status": http.StatusText(c.Response().StatusCode()) + ": " + fmt.Sprint(c.Response().StatusCode()),
"Message": errMsg.Error.Message,
"Type": errMsg.Error.Type,
"Cause": errMsg.Error.Cause,
})
return nil
}
c.Set("Content-Type", "application/json")
return c.JSON(errMsg)
}
return err
}
}

245
handlers/error_page.html Normal file
View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/styles.css" />
<script src="/script.js" defer></script>
<script>
const handleThemeChange = () => {
let theme = localStorage.getItem("theme");
if (theme === null) {
localStorage.setItem("theme", "system");
theme = "system";
}
if (
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
handleThemeChange();
</script>
<title>ladder | error</title>
</head>
<body
class="antialiased bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-200"
>
<div class="flex flex-col gap-4 max-w-3xl mx-auto pt-10">
<div class="flex justify-between place-items-center">
<div
class="hover:drop-shadow-[0_0px_4px_rgba(122,167,209,.3)] ring-offset-white dark:ring-offset-slate-900 transition-colors duration-300 focus:outline-none focus:ring ring-offset-2"
>
<div class="flex">
<a
href="/"
aria-label="ladder"
class="flex -ml-2 h-8 font-extrabold tracking-tight no-underline focus:outline-none ring-offset-white dark:ring-offset-slate-900 focus:ring ring-offset-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512"
class="h-8 focus:outline-none focus:ring ring-offset-white dark:ring-offset-slate-900 ring-offset-2"
>
<path
fill="#7AA7D1"
d="M262.074 485.246C254.809 485.265 247.407 485.534 240.165 484.99L226.178 483.306C119.737 468.826 34.1354 383.43 25.3176 274.714C24.3655 262.975 23.5876 253.161 24.3295 241.148C31.4284 126.212 123.985 31.919 238.633 24.1259L250.022 23.8366C258.02 23.8001 266.212 23.491 274.183 24.1306C320.519 27.8489 366.348 45.9743 402.232 75.4548L416.996 88.2751C444.342 114.373 464.257 146.819 475.911 182.72L480.415 197.211C486.174 219.054 488.67 242.773 487.436 265.259L486.416 275.75C478.783 352.041 436.405 418.1 369.36 455.394L355.463 462.875C326.247 477.031 294.517 484.631 262.074 485.246ZM253.547 72.4475C161.905 73.0454 83.5901 144.289 73.0095 234.5C69.9101 260.926 74.7763 292.594 83.9003 317.156C104.53 372.691 153.9 416.616 211.281 430.903C226.663 434.733 242.223 436.307 258.044 436.227C353.394 435.507 430.296 361.835 438.445 267.978C439.794 252.442 438.591 236.759 435.59 221.5C419.554 139.955 353.067 79.4187 269.856 72.7052C264.479 72.2714 258.981 72.423 253.586 72.4127L253.547 72.4475Z"
/>
<path
fill="#7AA7D1"
d="M153.196 310.121L133.153 285.021C140.83 283.798 148.978 285.092 156.741 284.353L156.637 277.725L124.406 278.002C123.298 277.325 122.856 276.187 122.058 275.193L116.089 267.862C110.469 260.975 103.827 254.843 98.6026 247.669C103.918 246.839 105.248 246.537 111.14 246.523L129.093 246.327C130.152 238.785 128.62 240.843 122.138 240.758C111.929 240.623 110.659 242.014 105.004 234.661L97.9953 225.654C94.8172 221.729 91.2219 218.104 88.2631 214.005C84.1351 208.286 90.1658 209.504 94.601 209.489L236.752 209.545C257.761 209.569 268.184 211.009 285.766 221.678L285.835 206.051C285.837 197.542 286.201 189.141 284.549 180.748C280.22 158.757 260.541 143.877 240.897 135.739C238.055 134.561 232.259 133.654 235.575 129.851C244.784 119.288 263.680 111.990 277.085 111.105C288.697 109.828 301.096 113.537 311.75 117.703C360.649 136.827 393.225 183.042 398.561 234.866C402.204 270.253 391.733 308.356 367.999 335.1C332.832 374.727 269.877 384.883 223.294 360.397C206.156 351.388 183.673 333.299 175.08 316.6C173.511 313.551 174.005 313.555 170.443 313.52L160.641 313.449C158.957 313.435 156.263 314.031 155.122 312.487L153.196 310.121Z"
/>
</svg>
<span class="text-3xl ml-1 text-[#7AA7D1] leading-8 align-middle"
>ladder</span
>
</a>
</div>
</div>
<div class="flex justify-center z-10">
<div class="relative" id="dropdown">
<button
aria-expanded="false"
id="dropdownButton"
aria-label="Toggle dropdown menu"
onclick="toggleDropdown()"
type="button"
class="inline-flex items-center justify-center whitespace-nowrap rounded-full h-12 px-4 py-2 text-sm font-medium text-slate-600 dark:text-slate-400 ring-offset-white dark:ring-offset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-700 hover:text-slate-500 dark:hover:text-slate-200"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</button>
<div
id="dropdown_panel"
class="hidden absolute right-0 mt-2 w-52 rounded-md bg-white dark:bg-slate-900 shadow-md border border-slate-400 dark:border-slate-700"
>
<div
class="flex flex-col gap-2 w-full first-of-type:rounded-t-md last-of-type:rounded-b-md px-4 py-2.5 text-left text-sm"
>
Appearance
<div class="grid grid-cols-4 gap-2">
<div>
<input
type="radio"
name="theme"
id="light"
value="light"
class="peer hidden"
/>
<label
for="light"
tabindex="0"
title="Light"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-sm text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
</label>
</div>
<div>
<input
type="radio"
name="theme"
id="dark"
value="dark"
class="peer hidden"
/>
<label
for="dark"
tabindex="0"
title="Dark"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-base text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</label>
</div>
<div>
<input
type="radio"
name="theme"
id="system"
value="system"
class="peer hidden"
checked
/>
<label
for="system"
tabindex="0"
title="System preference"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-lg text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="M12 8a2.83 2.83 0 0 0 4 4 4 4 0 1 1-4-4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.9 4.9 1.4 1.4" />
<path d="m17.7 17.7 1.4 1.4" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.3 17.7-1.4 1.4" />
<path d="m19.1 4.9-1.4 1.4" />
</svg>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col space-y-3">
<h1>Error</h1>
<div class="my-4"></div>
<pre
class="mx-auto px-6 py-4 space-y-3 whitespace-normal break-all text-red-500 dark:text-red-400"
>
<div><span class="underline">{{.Status}}</span></div>
<div>{{.Message}}</div>
<div><span class="underline">Type</span>:&nbsp;{{.Type}}</div>
<div><span class="underline">Cause</span>:&nbsp;{{.Cause}}</div>
</pre>
</div>
<footer class="mx-4 my-2 text-center text-slate-600 dark:text-slate-400">
<small>
<a
href="https://github.com/everywall"
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
>Everywall</a
>
|
<a
href="https://github.com/everywall/ladder"
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
>Source</a
>
| Code Licensed Under GPL v3.0
</small>
</footer>
</div>
</body>
</html>

18
handlers/favicon.go Normal file
View File

@@ -0,0 +1,18 @@
package handlers
import (
_ "embed"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/favicon"
)
//go:embed favicon.ico
var faviconData string
func Favicon() fiber.Handler {
return favicon.New(favicon.Config{
Data: []byte(faviconData),
URL: "/favicon.ico",
})
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,79 +1,326 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head> <meta charset="UTF-8" />
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="/styles.css" />
<title>ladder</title> <title>ladder</title>
<link rel="stylesheet" href="/styles.css"> <script src="/script.js" defer></script>
</head> <script>
const handleThemeChange = () => {
let theme = localStorage.getItem("theme");
if (theme === null) {
localStorage.setItem("theme", "system");
theme = "system";
}
if (
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
handleThemeChange();
</script>
</head>
<body class="antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900"> <body class="antialiased bg-white dark:bg-slate-900">
<div class="grid grid-cols-1 gap-4 max-w-3xl mx-auto pt-10"> <div class="flex flex-col gap-4 max-w-3xl mx-auto pt-10">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="250" viewBox="0 0 512 512"> <div class="place-self-end z-10">
<path fill="#7AA7D1" d="M262.074 485.246C254.809 485.265 247.407 485.534 240.165 484.99L226.178 483.306C119.737 468.826 34.1354 383.43 25.3176 274.714C24.3655 262.975 23.5876 253.161 24.3295 241.148C31.4284 126.212 123.985 31.919 238.633 24.1259L250.022 23.8366C258.02 23.8001 266.212 23.491 274.183 24.1306C320.519 27.8489 366.348 45.9743 402.232 75.4548L416.996 88.2751C444.342 114.373 464.257 146.819 475.911 182.72L480.415 197.211C486.174 219.054 488.67 242.773 487.436 265.259L486.416 275.75C478.783 352.041 436.405 418.1 369.36 455.394L355.463 462.875C326.247 477.031 294.517 484.631 262.074 485.246ZM253.547 72.4475C161.905 73.0454 83.5901 144.289 73.0095 234.5C69.9101 260.926 74.7763 292.594 83.9003 317.156C104.53 372.691 153.9 416.616 211.281 430.903C226.663 434.733 242.223 436.307 258.044 436.227C353.394 435.507 430.296 361.835 438.445 267.978C439.794 252.442 438.591 236.759 435.59 221.5C419.554 139.955 353.067 79.4187 269.856 72.7052C264.479 72.2714 258.981 72.423 253.586 72.4127L253.547 72.4475Z"/> <div class="relative" id="dropdown">
<path fill="#7AA7D1" d="M153.196 310.121L133.153 285.021C140.83 283.798 148.978 285.092 156.741 284.353L156.637 277.725L124.406 278.002C123.298 277.325 122.856 276.187 122.058 275.193L116.089 267.862C110.469 260.975 103.827 254.843 98.6026 247.669C103.918 246.839 105.248 246.537 111.14 246.523L129.093 246.327C130.152 238.785 128.62 240.843 122.138 240.758C111.929 240.623 110.659 242.014 105.004 234.661L97.9953 225.654C94.8172 221.729 91.2219 218.104 88.2631 214.005C84.1351 208.286 90.1658 209.504 94.601 209.489L236.752 209.545C257.761 209.569 268.184 211.009 285.766 221.678L285.835 206.051C285.837 197.542 286.201 189.141 284.549 180.748C280.22 158.757 260.541 143.877 240.897 135.739C238.055 134.561 232.259 133.654 235.575 129.851C244.784 119.288 263.680 111.990 277.085 111.105C288.697 109.828 301.096 113.537 311.75 117.703C360.649 136.827 393.225 183.042 398.561 234.866C402.204 270.253 391.733 308.356 367.999 335.1C332.832 374.727 269.877 384.883 223.294 360.397C206.156 351.388 183.673 333.299 175.08 316.6C173.511 313.551 174.005 313.555 170.443 313.52L160.641 313.449C158.957 313.435 156.263 314.031 155.122 312.487L153.196 310.121Z"/> <button
aria-expanded="false"
id="dropdownButton"
aria-label="Toggle dropdown menu"
onclick="toggleDropdown()"
type="button"
class="inline-flex items-center justify-center whitespace-nowrap rounded-full h-12 px-4 py-2 text-sm font-medium text-slate-600 dark:text-slate-400 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-700 hover:text-slate-500 dark:hover:text-slate-200"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg> </svg>
<header> </button>
<h1 class="text-center text-3xl sm:text-4xl font-extrabold text-slate-900 tracking-tight dark:text-slate-200">ladddddddder</h1>
</header> <div
<form id="inputForm" method="get" class="mx-4 relative"> id="dropdown_panel"
class="hidden absolute right-0 mt-2 w-52 rounded-md bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-200 shadow-md border border-slate-400 dark:border-slate-700"
>
<div
class="flex flex-col gap-2 w-full first-of-type:rounded-t-md last-of-type:rounded-b-md px-4 py-2.5 text-left text-sm"
>
Appearance
<div class="grid grid-cols-4 gap-2">
<div> <div>
<input type="text" id="inputField" placeholder="Proxy Search" name="inputField" class="w-full text-sm leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700" required autofocus> <input
<button id="clearButton" type="button" aria-label="Clear Search" title="Clear Search" class="hidden absolute inset-y-0 right-0 items-center pr-2 hover:text-slate-400 hover:dark:text-slate-300" tabindex="-1"> type="radio"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round""><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> name="theme"
id="light"
value="light"
class="peer hidden"
/>
<label
for="light"
tabindex="0"
title="Light"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-sm text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
</label>
</div>
<div>
<input
type="radio"
name="theme"
id="dark"
value="dark"
class="peer hidden"
/>
<label
for="dark"
tabindex="0"
title="Dark"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-base text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</label>
</div>
<div>
<input
type="radio"
name="theme"
id="system"
value="system"
class="peer hidden"
checked
/>
<label
for="system"
tabindex="0"
title="System preference"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-lg text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="M12 8a2.83 2.83 0 0 0 4 4 4 4 0 1 1-4-4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.9 4.9 1.4 1.4" />
<path d="m17.7 17.7 1.4 1.4" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.3 17.7-1.4 1.4" />
<path d="m19.1 4.9-1.4 1.4" />
</svg>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mx-auto -mt-12">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512"
class="h-[250px] hover:drop-shadow-[0_0px_10px_rgba(122,167,209,.3)] transition-colors duration-300"
>
<path
fill="#7AA7D1"
d="M262.074 485.246C254.809 485.265 247.407 485.534 240.165 484.99L226.178 483.306C119.737 468.826 34.1354 383.43 25.3176 274.714C24.3655 262.975 23.5876 253.161 24.3295 241.148C31.4284 126.212 123.985 31.919 238.633 24.1259L250.022 23.8366C258.02 23.8001 266.212 23.491 274.183 24.1306C320.519 27.8489 366.348 45.9743 402.232 75.4548L416.996 88.2751C444.342 114.373 464.257 146.819 475.911 182.72L480.415 197.211C486.174 219.054 488.67 242.773 487.436 265.259L486.416 275.75C478.783 352.041 436.405 418.1 369.36 455.394L355.463 462.875C326.247 477.031 294.517 484.631 262.074 485.246ZM253.547 72.4475C161.905 73.0454 83.5901 144.289 73.0095 234.5C69.9101 260.926 74.7763 292.594 83.9003 317.156C104.53 372.691 153.9 416.616 211.281 430.903C226.663 434.733 242.223 436.307 258.044 436.227C353.394 435.507 430.296 361.835 438.445 267.978C439.794 252.442 438.591 236.759 435.59 221.5C419.554 139.955 353.067 79.4187 269.856 72.7052C264.479 72.2714 258.981 72.423 253.586 72.4127L253.547 72.4475Z"
/>
<path
fill="#7AA7D1"
d="M153.196 310.121L133.153 285.021C140.83 283.798 148.978 285.092 156.741 284.353L156.637 277.725L124.406 278.002C123.298 277.325 122.856 276.187 122.058 275.193L116.089 267.862C110.469 260.975 103.827 254.843 98.6026 247.669C103.918 246.839 105.248 246.537 111.14 246.523L129.093 246.327C130.152 238.785 128.62 240.843 122.138 240.758C111.929 240.623 110.659 242.014 105.004 234.661L97.9953 225.654C94.8172 221.729 91.2219 218.104 88.2631 214.005C84.1351 208.286 90.1658 209.504 94.601 209.489L236.752 209.545C257.761 209.569 268.184 211.009 285.766 221.678L285.835 206.051C285.837 197.542 286.201 189.141 284.549 180.748C280.22 158.757 260.541 143.877 240.897 135.739C238.055 134.561 232.259 133.654 235.575 129.851C244.784 119.288 263.680 111.990 277.085 111.105C288.697 109.828 301.096 113.537 311.75 117.703C360.649 136.827 393.225 183.042 398.561 234.866C402.204 270.253 391.733 308.356 367.999 335.1C332.832 374.727 269.877 384.883 223.294 360.397C206.156 351.388 183.673 333.299 175.08 316.6C173.511 313.551 174.005 313.555 170.443 313.52L160.641 313.449C158.957 313.435 156.263 314.031 155.122 312.487L153.196 310.121Z"
/>
</svg>
</div>
<header>
<h1
class="text-center text-3xl sm:text-4xl font-extrabold text-slate-900 tracking-tight dark:text-slate-200 cursor-default"
>
ladddddddder
</h1>
</header>
<form id="inputForm" method="get" class="flex flex-col gap-2 mx-4">
<div class="relative">
<input
type="url"
id="inputField"
placeholder="Enter URL"
name="inputField"
class="w-full text-sm leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700"
autocomplete="off"
autofocus
required
/>
<button
id="clearButton"
type="reset"
aria-label="Clear Search"
title="Clear Search"
class="hidden absolute inset-y-0 right-0 items-center pr-2 text-slate-600 dark:text-slate-400 hover:text-slate-400 hover:dark:text-slate-300"
tabindex="-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div
class="flex flex-wrap-reverse mt-5 gap-x-10 gap-y-4 justify-center"
>
<button
type="button"
id="outlineButton"
class="inline-flex items-center justify-center h-11 px-8 whitespace-nowrap rounded-md text-sm font-medium text-slate-900 dark:text-slate-200 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-slate-200 dark:bg-slate-800 hover:bg-slate-200/90 dark:hover:bg-slate-800/90"
>
Create Outline
</button>
<button
type="submit"
id="submitButton"
class="inline-flex items-center justify-center h-11 px-8 whitespace-nowrap rounded-md text-sm font-medium text-slate-200 dark:text-slate-900 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-slate-800 dark:bg-slate-200 hover:bg-slate-800/90 dark:hover:bg-slate-200/90"
>
Proxy Search
</button> </button>
</div> </div>
</form> </form>
<footer class="mt-10 mx-4 text-center text-slate-600 dark:text-slate-400">
<p> <div class="flex flex-col justify-center my-4 mx-auto">
Code Licensed Under GPL v3.0 | <a
<a href="https://github.com/everywall/ladder" class="hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300">Source</a> | href="/playground"
<a href="https://github.com/everywall/ladder/releases" class="hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300">VERSION</a> class="inline-flex items-center justify-center h-8 px-8 whitespace-nowrap no-underline rounded-md text-sm font-medium text-slate-900 dark:text-slate-200 hover:text-slate-900 hover:dark:text-slate-200 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none hover:bg-slate-200/90 dark:hover:bg-slate-800/90 border border-slate-600 dark:border-slate-400"
</p> >
Experiment with modifiers in the playground
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4 ml-1"
>
<path d="M13 5H19V11" />
<path d="M19 5L5 19" />
</svg>
</a>
</div>
<footer class="mx-4 my-2 text-center text-slate-600 dark:text-slate-400">
<small>
<a
href="https://github.com/everywall"
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
>Everywall</a
>
|
<a
href="https://github.com/everywall/ladder"
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
>Source</a
>
| Code Licensed Under GPL v3.0
</small>
</footer> </footer>
</div> </div>
<script> <script>
document.getElementById('inputForm').addEventListener('submit', function (e) { function validateAndRedirect(destination) {
let url = inputField.value;
const redirectUrl =
destination === "outline" ? "/outline/" + url : "/" + url;
window.location.href = redirectUrl;
return true;
}
document
.getElementById("inputForm")
.addEventListener("submit", function (e) {
e.preventDefault(); e.preventDefault();
let url = document.getElementById('inputField').value; validateAndRedirect("default");
if (url.indexOf('http') === -1) {
url = 'https://' + url;
}
window.location.href = '/' + url;
return false;
}); });
document.getElementById('inputField').addEventListener('input', function() {
const clearButton = document.getElementById('clearButton'); document
if (this.value.trim().length > 0) { .getElementById("outlineButton")
clearButton.style.display = 'block'; .addEventListener("click", function () {
} else { validateAndRedirect("outline");
clearButton.style.display = 'none';
}
});
document.getElementById('clearButton').addEventListener('click', function() {
document.getElementById('inputField').value = '';
this.style.display = 'none';
document.getElementById('inputField').focus();
}); });
</script> </script>
</body>
<style>
@media (prefers-color-scheme: light) {
body {
background-color: #ffffff;
color: #333333;
}
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1a202c;
color: #ffffff;
}
}
</style>
</body>
</html> </html>

32
handlers/outline.go Normal file
View File

@@ -0,0 +1,32 @@
package handlers
import (
rx "github.com/everywall/ladder/proxychain/requestmodifiers"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
"github.com/gofiber/fiber/v2"
)
func NewOutlineHandler(path string, opts *ProxyOptions) fiber.Handler {
return func(c *fiber.Ctx) error {
return proxychain.
NewProxyChain().
WithAPIPath(path).
SetDebugLogging(opts.Verbose).
SetRequestModifications(
rx.MasqueradeAsGoogleBot(),
rx.ForwardRequestHeaders(),
rx.SpoofReferrerFromGoogleSearch(),
).
AddResponseModifications(
tx.SetResponseHeader("content-type", "text/html"),
tx.DeleteIncomingCookies(),
tx.RewriteHTMLResourceURLs(),
tx.GenerateReadableOutline(), // <-- this response modification does the outline rendering
).
SetFiberCtx(c).
Execute()
}
}

View File

@@ -0,0 +1,491 @@
const modifierContainer = document.getElementById("modifierContainer");
const modalContainer = document.getElementById("modalContainer");
const modalBody = document.getElementById("modal-body");
const modalContent = document.getElementById("modal-content");
const modalSubmitButton = document.getElementById("modal-submit");
const modalClose = document.getElementById("modal-close");
let hasFetched = false;
let payload = {
requestmodifications: [],
responsemodifications: [],
};
let ninjaData = [];
initialize();
// Rerun handleThemeChange() so style is applied to Ninja Keys
handleThemeChange();
// Add event listener to the iframe so it closes dropdown when clicked
closeDropdownOnClickWithinIframe();
async function initialize() {
if (!hasFetched) {
try {
await fetchPayload();
hasFetched = true;
} catch (error) {
console.error("Fetch error:", error);
}
}
}
function closeDropdownOnClickWithinIframe() {
const iframe = document.getElementById("resultIframe");
iframe.contentWindow.document.addEventListener(
"click",
() => {
if (
!document.getElementById("dropdown_panel").classList.contains("hidden")
) {
toggleDropdown();
}
},
true
);
}
async function fetchPayload() {
try {
const response = await fetch("/api/modifiers");
const data = await response.json();
Object.entries(data.result.requestmodifiers ?? []).forEach(([_, value]) => {
addModifierToNinjaData(
value.name,
value.description,
value.params,
"requestmodifications"
);
});
Object.entries(data.result.responsemodifiers ?? []).forEach(
([_, value]) => {
addModifierToNinjaData(
value.name,
value.description,
value.params,
"responsemodifications"
);
}
);
return data;
} catch (error) {
console.error("Fetch error:", error);
throw error;
}
}
async function submitForm() {
if (!document.getElementById("inputForm").checkValidity()) {
return;
}
try {
const response = await fetch("/playground/" + inputField.value, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Request failed");
}
const result = await response.text();
updateResultIframe(result);
} catch (error) {
console.error(error);
}
}
function updateResultIframe(result) {
const resultIframe = parent.document.getElementById("resultIframe");
resultIframe.contentDocument.open();
resultIframe.contentDocument.write(result);
closeDropdownOnClickWithinIframe();
resultIframe.contentDocument.close();
}
document.getElementById("inputForm").addEventListener("submit", function (e) {
e.preventDefault();
submitForm();
});
if (navigator.userAgent.includes("Mac")) {
document.getElementById("ninjaKey").textContent = "⌘";
} else {
document.getElementById("ninjaKey").textContent = "Ctrl";
}
function downloadYaml() {
function jsonToYaml(payload) {
const jsonObject = {
rules: [
{
domains: [hostname],
responsemodifications: [],
requestmodifications: [],
...payload,
},
],
};
return jsyaml.dump(jsonObject);
}
if (!document.getElementById("inputForm").checkValidity()) {
alert("Please enter a valid URL.");
return;
}
const hostname = new URL(inputField.value).hostname;
const ruleHostname = hostname.replace(/^www\./, "").replace(/\./g, "-");
const yamlString = jsonToYaml(payload);
const blob = new Blob([yamlString], { type: "text/yaml;charset=utf-8" });
const href = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = href;
link.download = `${ruleHostname}.yaml`;
link.click();
URL.revokeObjectURL(href);
}
function getValues(type, id, description, params) {
const focusTrap = trap(modalBody);
let values = [];
let existingValues = [];
const inputs = [];
const inputEventListeners = [];
function closeModal() {
focusTrap.destroy();
modalBody.removeEventListener("keydown", handleKeyboardEvents);
modalContainer.removeEventListener("click", handleClickOutside);
modalSubmitButton.removeEventListener("click", closeModal);
modalClose.removeEventListener("click", closeModal);
inputEventListeners.forEach((listener, index) => {
if (listener !== undefined && inputs[index] !== undefined)
inputs[index].removeEventListener("input", listener);
});
modalContent.classList.remove("relative", "h-[220px]");
inputEventListeners.length = 0;
inputs.length = 0;
modalContainer.classList.add("hidden");
modalContent.innerHTML = "";
}
function handleClickOutside(e) {
if (modalBody !== null && !modalBody.contains(e.target)) {
closeModal();
}
}
function handleKeyboardEvents(e) {
if (e.key === "Escape") {
closeModal();
}
if (e.key === "Enter") {
if (e.target.tagName.toLowerCase() === "textarea") {
return;
} else {
modalSubmitButton.click();
}
}
}
document.getElementById("modal-title").textContent = id;
document.getElementById("modal-description").textContent = description;
existingValues =
payload[type].find(
(modifier) => modifier.name === id && modifier.params !== undefined
)?.params ?? [];
params.map((param, i) => {
function textareaEventListener(e) {
const codeElement = document.querySelector("code");
let text = e.target.value;
if (text[text.length - 1] == "\n") {
text += " ";
}
codeElement.innerHTML = text
.replace(new RegExp("&", "g"), "&amp;")
.replace(new RegExp("<", "g"), "&lt;");
Prism.highlightElement(codeElement);
values[i] = text;
syncScroll(e.target);
}
function textareaKeyEventListener(e) {
if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
let text = e.target.value;
const start = e.target.selectionStart;
const end = e.target.selectionEnd;
e.target.value = text.substring(0, start) + "\t" + text.substring(end);
e.target.setSelectionRange(start + 1, start + 1);
e.target.dispatchEvent(new Event("input"));
}
syncScroll(e.target);
}
function syncScroll(el) {
const codeElement = document.querySelector("code");
codeElement.scrollTop = el.scrollTop;
codeElement.scrollLeft = el.scrollLeft;
}
function inputEventListener(e) {
if (e.key !== "Enter") {
values[i] = e.target.value;
}
}
const label = document.createElement("label");
label.textContent = param.name;
label.setAttribute("for", `input-${i}`);
let input;
if (param.name === "js") {
input = document.createElement("textarea");
input.type = "textarea";
input.setAttribute("spellcheck", "false");
input.placeholder = "Enter your JavaScript injection code ...";
input.classList.add(
"h-[200px]",
"w-full",
"font-mono",
"whitespace-nowrap",
"font-semibold",
"absolute",
"text-base",
"leading-6",
"rounded-md",
"ring-1",
"ring-slate-900/10",
"shadow-sm",
"z-10",
"p-4",
"m-0",
"my-2",
"bg-transparent",
"dark:bg-transparent",
"text-transparent",
"overflow-auto",
"resize-none",
"caret-white",
"hover:ring-slate-300",
"hyphens-none"
);
input.style.tabSize = "4";
} else {
input = document.createElement("input");
input.type = "text";
input.classList.add(
"w-full",
"text-sm",
"leading-6",
"text-slate-400",
"rounded-md",
"ring-1",
"ring-slate-900/10",
"shadow-sm",
"py-1.5",
"pl-2",
"pr-3",
"mt-0",
"hover:ring-slate-300",
"dark:bg-slate-800",
"dark:highlight-white/5",
"overflow-auto"
);
}
input.id = `input-${i}`;
input.value = existingValues[i] ?? "";
modalContent.appendChild(label);
modalContent.appendChild(input);
if (input.type === "textarea") {
label.classList.add("sr-only", "hidden");
preElement = document.createElement("pre");
codeElement = document.createElement("code");
preElement.setAttribute("aria-hidden", "true");
preElement.classList.add(
"bg-[#2d2d2d]",
"dark:bg-[#2d2d2d]",
"h-[200px]",
"w-full",
"rounded-md",
"ring-1",
"ring-slate-900/10",
"shadow-sm",
"p-0",
"m-0",
"my-2",
"font-mono",
"text-base",
"leading-6",
"overflow-auto",
"whitespace-nowrap",
"font-semibold",
"absolute",
"z-0",
"hyphens-none"
);
modalContent.classList.add("relative", "h-[220px]");
preElement.setAttribute("tabindex", "-1");
codeElement.classList.add(
"language-javascript",
"absolute",
"w-full",
"font-mono",
"text-base",
"leading-6",
"z-0",
"p-4",
"-mx-4",
"-my-4",
"h-full",
"whitespace-nowrap",
"overflow-auto",
"hyphens-none"
);
preElement.appendChild(codeElement);
modalContent.appendChild(preElement);
codeElement.innerHTML = input.value
.replace(new RegExp("&", "g"), "&amp;")
.replace(new RegExp("<", "g"), "&lt;");
Prism.highlightElement(codeElement);
input.addEventListener("input", textareaEventListener);
input.addEventListener("keydown", textareaKeyEventListener);
input.addEventListener("scroll", () => syncScroll(input));
inputEventListeners.push(
textareaEventListener,
textareaKeyEventListener,
syncScroll
);
} else {
input.addEventListener("input", inputEventListener);
inputEventListeners.push(inputEventListener);
}
inputs.push(input);
});
modalContainer.classList.remove("hidden");
document.getElementById("input-0").focus();
return new Promise((resolve) => {
modalBody.addEventListener("keydown", handleKeyboardEvents);
modalContainer.addEventListener("click", handleClickOutside);
modalClose.addEventListener("click", () => {
closeModal();
});
modalSubmitButton.addEventListener("click", (e) => {
inputs.forEach((input, i) => {
values[i] = input.value;
});
resolve(values);
closeModal();
});
});
}
function toggleModifier(type, id, params = []) {
function pillClickHandler(pill) {
toggleModifier(pill.getAttribute("type"), pill.id);
pill.removeEventListener("click", () => pillClickHandler(pill));
pill.remove();
}
function createPill(type, id) {
const pill = document.createElement("span");
pill.classList.add(
"inline-flex",
"items-center",
"rounded-md",
"bg-slate-100",
"dark:bg-slate-800",
"px-2",
"py-1",
"h-4",
"text-xs",
"font-medium",
"border",
"border-slate-400",
"dark:border-slate-700",
"cursor-pointer"
);
pill.id = id;
pill.setAttribute("type", type);
pill.textContent = id;
modifierContainer.appendChild(pill);
pill.addEventListener("click", () => pillClickHandler(pill));
}
if (
params === undefined &&
payload[type].some((modifier) => modifier.name === id)
) {
payload[type] = payload[type].filter((modifier) => modifier.name !== id);
const existingPill = document.getElementById(id);
if (existingPill !== null) {
existingPill.removeEventListener("click", () => pillClickHandler(pill));
existingPill.remove();
}
} else {
const existingModifier = payload[type].find(
(modifier) => modifier.name === id
);
if (existingModifier) {
existingModifier.params = params;
} else {
payload[type].push({ name: id, params: params });
}
const existingPill = document.getElementById(id);
if (existingPill === null) {
createPill(type, id);
}
}
submitForm();
}
function addModifierToNinjaData(id, description, params, type) {
const section =
type === "requestmodifications"
? "Request Modifiers"
: "Response Modifiers";
const modifier = {
id: id,
title: id,
section: section,
handler: () => {
if (Object.keys(params).length === 0) {
toggleModifier(type, id);
} else {
if (params[0].name === "_") {
toggleModifier(type, id, (params = [""]));
} else {
getValues(type, id, description, params).then((values) => {
if (Object.keys(values).length === 0) return;
toggleModifier(type, id, values);
});
}
}
},
};
ninjaData.push(modifier);
}
const ninja = document.querySelector("ninja-keys");
ninja.data = ninjaData;
document.getElementById("btnNinja").addEventListener("click", () => {
ninja.open();
});

42
handlers/playground.go Normal file
View File

@@ -0,0 +1,42 @@
package handlers
import (
_ "embed"
"github.com/everywall/ladder/proxychain"
ruleset_v2 "github.com/everywall/ladder/proxychain/ruleset"
"net/http"
"github.com/gofiber/fiber/v2"
)
//go:embed playground.html
var playgroundHtml string
func PlaygroundHandler(path string, opts *ProxyOptions) fiber.Handler {
return func(c *fiber.Ctx) error {
if c.Method() == fiber.MethodGet {
c.Set("Content-Type", "text/html")
return c.SendString(playgroundHtml)
} else if c.Method() == fiber.MethodPost {
var modificationData ruleset_v2.Rule
if err := c.BodyParser(&modificationData); err != nil {
return err
}
c.Method(fiber.MethodGet)
return proxychain.
NewProxyChain().
SetFiberCtx(c).
WithAPIPath(path).
AddOnceRequestModifications(modificationData.RequestModifications...).
AddOnceResponseModifications(modificationData.ResponseModifications...).
Execute()
}
return c.Status(http.StatusMethodNotAllowed).SendString("Method not allowed")
}
}

451
handlers/playground.html Normal file
View File

@@ -0,0 +1,451 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ladder | playground</title>
<link rel="stylesheet" href="/styles.css" />
<style>
#modifierContainer::-webkit-scrollbar {
width: 8px;
}
#modifierContainer::-webkit-scrollbar-track {
background: rgb(226 232 240);
border-radius: 5px;
}
#modifierContainer::-webkit-scrollbar-thumb {
background: rgb(100 116 139);
border-radius: 14px;
}
ninja-keys {
--ninja-accent-color: #7aa7d1;
}
</style>
<script src="/script.js" defer></script>
<script src="/playground-script.js" defer></script>
<script type="importmap">
{
"imports": {
"https://unpkg.com/lit-html@latest/directives/ref.js?module": "https://unpkg.com/lit-html@2.2.6/directives/ref.js?module"
}
}
</script>
<script type="module" src="https://unpkg.com/ninja-keys?module"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism-tomorrow.min.css"
integrity="sha512-kSwGoyIkfz4+hMo5jkJngSByil9jxJPKbweYec/UgS+S1EgE45qm4Gea7Ks2oxQ7qiYyyZRn66A9df2lMtjIsw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/prism.min.js"
integrity="sha512-UOoJElONeUNzQbbKQbjldDf9MwOHqxNz49NNJJ1d90yp+X9edsHyJoAs6O4K19CZGaIdjI5ohK+O2y5lBTW6uQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script>
const handleThemeChange = () => {
let theme = localStorage.getItem("theme");
if (theme === null) {
localStorage.setItem("theme", "system");
theme = "system";
}
if (
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
const ninjaKeys = document.querySelector("ninja-keys");
if (ninjaKeys !== null) ninjaKeys.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
const ninjaKeys = document.querySelector("ninja-keys");
if (ninjaKeys !== null) ninjaKeys.classList.remove("dark");
}
};
handleThemeChange();
</script>
</head>
<body
class="antialiased bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-200"
>
<div class="flex flex-col h-screen">
<div
class="fixed top-0 inset-x-0 h-48 flex-col gap-4 max-w-3xl mx-4 lg:mx-auto pt-4"
>
<header class="flex flex-col gap-2">
<div class="flex justify-between place-items-center">
<div
class="hover:drop-shadow-[0_0px_4px_rgba(122,167,209,.3)] ring-offset-white dark:ring-offset-slate-900 transition-colors duration-300 focus:outline-none focus:ring ring-offset-2"
>
<div class="flex">
<a
href="/"
aria-label="ladder"
class="flex -ml-2 h-8 font-extrabold tracking-tight no-underline focus:outline-none ring-offset-white dark:ring-offset-slate-900 focus:ring ring-offset-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512"
class="h-8 focus:outline-none focus:ring ring-offset-white dark:ring-offset-slate-900 ring-offset-2"
>
<path
fill="#7AA7D1"
d="M262.074 485.246C254.809 485.265 247.407 485.534 240.165 484.99L226.178 483.306C119.737 468.826 34.1354 383.43 25.3176 274.714C24.3655 262.975 23.5876 253.161 24.3295 241.148C31.4284 126.212 123.985 31.919 238.633 24.1259L250.022 23.8366C258.02 23.8001 266.212 23.491 274.183 24.1306C320.519 27.8489 366.348 45.9743 402.232 75.4548L416.996 88.2751C444.342 114.373 464.257 146.819 475.911 182.72L480.415 197.211C486.174 219.054 488.67 242.773 487.436 265.259L486.416 275.75C478.783 352.041 436.405 418.1 369.36 455.394L355.463 462.875C326.247 477.031 294.517 484.631 262.074 485.246ZM253.547 72.4475C161.905 73.0454 83.5901 144.289 73.0095 234.5C69.9101 260.926 74.7763 292.594 83.9003 317.156C104.53 372.691 153.9 416.616 211.281 430.903C226.663 434.733 242.223 436.307 258.044 436.227C353.394 435.507 430.296 361.835 438.445 267.978C439.794 252.442 438.591 236.759 435.59 221.5C419.554 139.955 353.067 79.4187 269.856 72.7052C264.479 72.2714 258.981 72.423 253.586 72.4127L253.547 72.4475Z"
/>
<path
fill="#7AA7D1"
d="M153.196 310.121L133.153 285.021C140.83 283.798 148.978 285.092 156.741 284.353L156.637 277.725L124.406 278.002C123.298 277.325 122.856 276.187 122.058 275.193L116.089 267.862C110.469 260.975 103.827 254.843 98.6026 247.669C103.918 246.839 105.248 246.537 111.14 246.523L129.093 246.327C130.152 238.785 128.62 240.843 122.138 240.758C111.929 240.623 110.659 242.014 105.004 234.661L97.9953 225.654C94.8172 221.729 91.2219 218.104 88.2631 214.005C84.1351 208.286 90.1658 209.504 94.601 209.489L236.752 209.545C257.761 209.569 268.184 211.009 285.766 221.678L285.835 206.051C285.837 197.542 286.201 189.141 284.549 180.748C280.22 158.757 260.541 143.877 240.897 135.739C238.055 134.561 232.259 133.654 235.575 129.851C244.784 119.288 263.680 111.990 277.085 111.105C288.697 109.828 301.096 113.537 311.75 117.703C360.649 136.827 393.225 183.042 398.561 234.866C402.204 270.253 391.733 308.356 367.999 335.1C332.832 374.727 269.877 384.883 223.294 360.397C206.156 351.388 183.673 333.299 175.08 316.6C173.511 313.551 174.005 313.555 170.443 313.52L160.641 313.449C158.957 313.435 156.263 314.031 155.122 312.487L153.196 310.121Z"
/>
</svg>
<span
class="text-3xl ml-1 text-[#7AA7D1] leading-8 align-middle"
>ladder</span
>
</a>
<span
class="text-3xl mx-1 text-[#7AA7D1] leading-8 align-middle"
>|</span
>
<a
href="/playground"
class="flex h-8 font-extrabold tracking-tight no-underline focus:outline-none focus:ring ring-offset-2 ring-offset-white dark:ring-offset"
>
<span class="text-3xl text-[#7AA7D1] leading-8 align-middle"
>playground</span
>
</a>
</div>
</div>
<div class="flex justify-center z-10">
<div class="relative" id="dropdown">
<button
aria-expanded="false"
id="dropdownButton"
aria-label="Toggle dropdown menu"
onclick="toggleDropdown()"
type="button"
class="inline-flex items-center justify-center whitespace-nowrap rounded-full h-12 px-4 py-2 text-sm font-medium text-slate-600 dark:text-slate-400 ring-offset-white dark:ring-offset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-700 hover:text-slate-500 dark:hover:text-slate-200"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</button>
<div
id="dropdown_panel"
class="hidden absolute right-0 mt-2 w-52 rounded-md bg-white dark:bg-slate-900 shadow-md border border-slate-400 dark:border-slate-700"
>
<div
class="flex flex-col gap-2 w-full first-of-type:rounded-t-md last-of-type:rounded-b-md px-4 py-2.5 text-left text-sm"
>
Appearance
<div class="grid grid-cols-4 gap-2">
<div>
<input
type="radio"
name="theme"
id="light"
value="light"
class="peer hidden"
/>
<label
for="light"
tabindex="0"
title="Light"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-sm text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
</label>
</div>
<div>
<input
type="radio"
name="theme"
id="dark"
value="dark"
class="peer hidden"
/>
<label
for="dark"
tabindex="0"
title="Dark"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-base text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</label>
</div>
<div>
<input
type="radio"
name="theme"
id="system"
value="system"
class="peer hidden"
checked
/>
<label
for="system"
tabindex="0"
title="System preference"
class="flex items-end justify-center h-10 w-10 cursor-pointer select-none rounded-md p-2 text-lg text-slate-600 dark:text-slate-200 text-center hover:bg-slate-200 dark:hover:bg-slate-700 peer-checked:bg-slate-200 dark:peer-checked:bg-slate-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
>
<path d="M12 8a2.83 2.83 0 0 0 4 4 4 4 0 1 1-4-4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.9 4.9 1.4 1.4" />
<path d="m17.7 17.7 1.4 1.4" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.3 17.7-1.4 1.4" />
<path d="m19.1 4.9-1.4 1.4" />
</svg>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<form id="inputForm" method="get" class="flex flex-col gap-2 mx-4">
<div class="flex gap-6">
<div class="flex-1 relative">
<input
type="url"
id="inputField"
placeholder="Enter URL"
name="inputField"
class="w-full text-sm leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700"
autocomplete="off"
autofocus
required
/>
<button
id="clearButton"
type="reset"
aria-label="Clear Search"
title="Clear Search"
class="hidden absolute inset-y-0 right-0 items-center pr-2 text-slate-600 dark:text-slate-400 hover:text-slate-400 hover:dark:text-slate-300"
tabindex="-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<button
id="btnNinja"
type="button"
class="inline-flex items-center justify-center h-8 px-8 my-auto whitespace-nowrap rounded-xl text-sm font-medium text-slate-900 dark:text-slate-200 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-white dark:bg-slate-900 hover:bg-slate-200/90 dark:hover:bg-slate-800/90 border hover:bg-slate-200 dark:hover:bg-slate-500"
>
Press &nbsp;<span id="ninjaKey"></span>&nbsp; + K to Apply
Modifiers
</button>
</div>
</form>
<div class="mt-2 mx-4 flex justify-between gap-2">
<div
id="modifierContainer"
class="flex flex-wrap grow overflow-y-auto max-h-14 w-full gap-1"
></div>
<button
type="button"
class="inline-flex items-center justify-center h-11 px-4 py-2 whitespace-nowrap rounded-md text-sm font-medium text-slate-200 dark:text-slate-900 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-slate-800 dark:bg-slate-200 hover:bg-slate-800/90 dark:hover:bg-slate-200/90"
onclick="downloadYaml()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="pl-0 mr-2 w-5 h-5"
>
<path
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
/>
<polyline points="14 2 14 8 20 8" />
<path d="M12 18v-6" />
<path d="m9 15 3 3 3-3" />
</svg>
Export Rule
</button>
</div>
</header>
</div>
<iframe
name="resultIframe"
id="resultIframe"
title="resultIframe"
class="mt-48 h-[calc(100vh-14.5rem)] w-full overflow-x-hidden overflow-y-auto border-t-2 border-b-2 border-slate-400 dark:border-slate-700"
></iframe>
<footer class="mx-4 my-2 text-center text-slate-600 dark:text-slate-400">
<small>
<a
href="https://github.com/everywall"
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
>Everywall</a
>
|
<a
href="https://github.com/everywall/ladder"
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
>Source</a
>
| Code Licensed Under GPL v3.0
</small>
</footer>
<div
id="modalContainer"
tabindex="-1"
class="hidden relative z-10"
aria-labelledby="modal"
role="dialog"
aria-modal="true"
aria-hidden="true"
>
<div class="fixed inset-0 bg-slate-500/50 transition-opacity"></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-center justify-center p-4 text-center sm:p-0"
>
<div
id="modal-body"
class="relative transform overflow-hidden rounded-lg bg-white dark:bg-slate-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
>
<form>
<div class="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div class="mt-3 text-left sm:ml-4 sm:mt-0">
<div class="flex w-full justify-between items-center">
<h3 id="modal-title">MODAL HEADER</h3>
<button
type="button"
id="modal-close"
class="ml-2 text-slate-600 dark:text-slate-400 hover:text-slate-400 hover:dark:text-slate-300 bg-transparent text-sm w-6 h-6 inline-flex justify-center items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<div class="mt-3">
<p id="modal-description">DESCRIPTION</p>
</div>
<div
class="my-2 flex flex-col gap-2 w-full"
id="modal-content"
></div>
</div>
</div>
<div
class="bg-slate-200 dark:bg-slate-800 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"
>
<button
type="button"
id="modal-submit"
class="inline-flex items-center justify-center h-11 px-4 py-2 whitespace-nowrap rounded-md text-sm font-medium text-slate-200 dark:text-slate-900 ring-offset-white dark:ring-offset-slate-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-slate-800 dark:bg-slate-200 hover:bg-slate-800/90 dark:hover:bg-slate-200/90"
>
Apply modification
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<ninja-keys> </ninja-keys>
</div>
</body>
</html>

View File

@@ -1,364 +1,76 @@
package handlers package handlers
import ( import (
"fmt" rx "github.com/everywall/ladder/proxychain/requestmodifiers"
"io" tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"log"
"net/http" "github.com/everywall/ladder/proxychain"
"net/url" ruleset_v2 "github.com/everywall/ladder/proxychain/ruleset"
"os"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gopkg.in/yaml.v3"
) )
var ( type ProxyOptions struct {
UserAgent = getenv("USER_AGENT", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)") Ruleset ruleset_v2.IRuleset
ForwardedFor = getenv("X_FORWARDED_FOR", "66.249.66.1") Verbose bool
rulesSet = loadRules()
allowedDomains = strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",")
)
// extracts a URL from the request ctx. If the URL in the request
// is a relative path, it reconstructs the full URL using the referer header.
func extractUrl(c *fiber.Ctx) (string, error) {
// try to extract url-encoded
reqUrl, err := url.QueryUnescape(c.Params("*"))
if err != nil {
// fallback
reqUrl = c.Params("*")
}
// Extract the actual path from req ctx
urlQuery, err := url.Parse(reqUrl)
if err != nil {
return "", fmt.Errorf("error parsing request URL '%s': %v", reqUrl, err)
}
isRelativePath := urlQuery.Scheme == ""
// eg: https://localhost:8080/images/foobar.jpg -> https://realsite.com/images/foobar.jpg
if isRelativePath {
// Parse the referer URL from the request header.
refererUrl, err := url.Parse(c.Get("referer"))
if err != nil {
return "", fmt.Errorf("error parsing referer URL from req: '%s': %v", reqUrl, err)
}
// Extract the real url from referer path
realUrl, err := url.Parse(strings.TrimPrefix(refererUrl.Path, "/"))
if err != nil {
return "", fmt.Errorf("error parsing real URL from referer '%s': %v", refererUrl.Path, err)
}
// reconstruct the full URL using the referer's scheme, host, and the relative path / queries
fullUrl := &url.URL{
Scheme: realUrl.Scheme,
Host: realUrl.Host,
Path: urlQuery.Path,
RawQuery: urlQuery.RawQuery,
}
if os.Getenv("LOG_URLS") == "true" {
log.Printf("modified relative URL: '%s' -> '%s'", reqUrl, fullUrl.String())
}
return fullUrl.String(), nil
}
// default behavior:
// eg: https://localhost:8080/https://realsite.com/images/foobar.jpg -> https://realsite.com/images/foobar.jpg
return urlQuery.String(), nil
} }
func ProxySite(c *fiber.Ctx) error { func NewProxySiteHandler(opts *ProxyOptions) fiber.Handler {
// Get the url from the URL /*
url, err := extractUrl(c) var rs ruleset.RuleSet
if opts.RulesetPath != "" {
r, err := ruleset.NewRuleset(opts.RulesetPath)
if err != nil { if err != nil {
log.Println("ERROR In URL extraction:", err) panic(err)
}
rs = r
}
*/
return func(c *fiber.Ctx) error {
proxychain := proxychain.
NewProxyChain().
SetFiberCtx(c).
SetDebugLogging(opts.Verbose).
SetRequestModifications(
//rx.SpoofJA3fingerprint(ja3, "Googlebot"),
rx.AddCacheBusterQuery(),
rx.MasqueradeAsGoogleBot(),
rx.ForwardRequestHeaders(),
rx.DeleteOutgoingCookies(),
rx.SpoofReferrerFromRedditPost(),
//rx.SpoofReferrerFromLinkedInPost(),
//rx.RequestWaybackMachine(),
//rx.RequestArchiveIs(),
).
AddResponseModifications(
tx.ForwardResponseHeaders(),
tx.BlockThirdPartyScripts(),
tx.DeleteIncomingCookies(),
tx.DeleteLocalStorageData(),
tx.DeleteSessionStorageData(),
tx.BypassCORS(),
tx.BypassContentSecurityPolicy(),
tx.RewriteHTMLResourceURLs(),
tx.PatchDynamicResourceURLs(),
tx.PatchTrackerScripts(),
//tx.BlockElementRemoval(".article-content"), // techcrunch
//tx.BlockElementRemoval(".available-content"), // substack
// tx.SetContentSecurityPolicy("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"),
)
// no options passed in, return early
if opts == nil {
return proxychain.Execute()
} }
queries := c.Queries() // load ruleset
body, _, resp, err := fetchSite(url, queries) rule, exists := opts.Ruleset.GetRule(proxychain.Request.URL)
if err != nil { if exists {
log.Println("ERROR:", err) proxychain.AddOnceRequestModifications(rule.RequestModifications...)
c.SendStatus(fiber.StatusInternalServerError) proxychain.AddOnceResponseModifications(rule.ResponseModifications...)
return c.SendString(err.Error())
} }
c.Set("Content-Type", resp.Header.Get("Content-Type")) return proxychain.Execute()
c.Set("Content-Security-Policy", resp.Header.Get("Content-Security-Policy")) }
return c.SendString(body)
}
func modifyURL(uri string, rule Rule) (string, error) {
newUrl, err := url.Parse(uri)
if err != nil {
return "", err
}
for _, urlMod := range rule.UrlMods.Domain {
re := regexp.MustCompile(urlMod.Match)
newUrl.Host = re.ReplaceAllString(newUrl.Host, urlMod.Replace)
}
for _, urlMod := range rule.UrlMods.Path {
re := regexp.MustCompile(urlMod.Match)
newUrl.Path = re.ReplaceAllString(newUrl.Path, urlMod.Replace)
}
v := newUrl.Query()
for _, query := range rule.UrlMods.Query {
if query.Value == "" {
v.Del(query.Key)
continue
}
v.Set(query.Key, query.Value)
}
newUrl.RawQuery = v.Encode()
if rule.GoogleCache {
newUrl, err = url.Parse("https://webcache.googleusercontent.com/search?q=cache:" + newUrl.String())
if err != nil {
return "", err
}
}
return newUrl.String(), nil
}
func fetchSite(urlpath string, queries map[string]string) (string, *http.Request, *http.Response, error) {
urlQuery := "?"
if len(queries) > 0 {
for k, v := range queries {
urlQuery += k + "=" + v + "&"
}
}
urlQuery = strings.TrimSuffix(urlQuery, "&")
urlQuery = strings.TrimSuffix(urlQuery, "?")
u, err := url.Parse(urlpath)
if err != nil {
return "", nil, nil, err
}
if len(allowedDomains) > 0 && !StringInSlice(u.Host, allowedDomains) {
return "", nil, nil, fmt.Errorf("domain not allowed. %s not in %s", u.Host, allowedDomains)
}
if os.Getenv("LOG_URLS") == "true" {
log.Println(u.String() + urlQuery)
}
// Modify the URI according to ruleset
rule := fetchRule(u.Host, u.Path)
url, err := modifyURL(u.String()+urlQuery, rule)
if err != nil {
return "", nil, nil, err
}
// Fetch the site
client := &http.Client{}
req, _ := http.NewRequest("GET", url, nil)
if rule.Headers.UserAgent != "" {
req.Header.Set("User-Agent", rule.Headers.UserAgent)
} else {
req.Header.Set("User-Agent", UserAgent)
}
if rule.Headers.XForwardedFor != "" {
if rule.Headers.XForwardedFor != "none" {
req.Header.Set("X-Forwarded-For", rule.Headers.XForwardedFor)
}
} else {
req.Header.Set("X-Forwarded-For", ForwardedFor)
}
if rule.Headers.Referer != "" {
if rule.Headers.Referer != "none" {
req.Header.Set("Referer", rule.Headers.Referer)
}
} else {
req.Header.Set("Referer", u.String())
}
if rule.Headers.Cookie != "" {
req.Header.Set("Cookie", rule.Headers.Cookie)
}
resp, err := client.Do(req)
if err != nil {
return "", nil, nil, err
}
defer resp.Body.Close()
bodyB, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, nil, err
}
if rule.Headers.CSP != "" {
log.Println(rule.Headers.CSP)
resp.Header.Set("Content-Security-Policy", rule.Headers.CSP)
}
//log.Print("rule", rule) TODO: Add a debug mode to print the rule
body := rewriteHtml(bodyB, u, rule)
return body, req, resp, nil
}
func rewriteHtml(bodyB []byte, u *url.URL, rule Rule) string {
// Rewrite the HTML
body := string(bodyB)
// images
imagePattern := `<img\s+([^>]*\s+)?src="(/)([^"]*)"`
re := regexp.MustCompile(imagePattern)
body = re.ReplaceAllString(body, fmt.Sprintf(`<img $1 src="%s$3"`, "/https://"+u.Host+"/"))
// scripts
scriptPattern := `<script\s+([^>]*\s+)?src="(/)([^"]*)"`
reScript := regexp.MustCompile(scriptPattern)
body = reScript.ReplaceAllString(body, fmt.Sprintf(`<script $1 script="%s$3"`, "/https://"+u.Host+"/"))
// body = strings.ReplaceAll(body, "srcset=\"/", "srcset=\"/https://"+u.Host+"/") // TODO: Needs a regex to rewrite the URL's
body = strings.ReplaceAll(body, "href=\"/", "href=\"/https://"+u.Host+"/")
body = strings.ReplaceAll(body, "url('/", "url('/https://"+u.Host+"/")
body = strings.ReplaceAll(body, "url(/", "url(/https://"+u.Host+"/")
body = strings.ReplaceAll(body, "href=\"https://"+u.Host, "href=\"/https://"+u.Host+"/")
if os.Getenv("RULESET") != "" {
body = applyRules(body, rule)
}
return body
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if len(value) == 0 {
return fallback
}
return value
}
func loadRules() RuleSet {
rulesUrl := os.Getenv("RULESET")
if rulesUrl == "" {
RulesList := RuleSet{}
return RulesList
}
log.Println("Loading rules")
var ruleSet RuleSet
if strings.HasPrefix(rulesUrl, "http") {
resp, err := http.Get(rulesUrl)
if err != nil {
log.Println("ERROR:", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
log.Println("ERROR:", resp.StatusCode, rulesUrl)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("ERROR:", err)
}
yaml.Unmarshal(body, &ruleSet)
if err != nil {
log.Println("ERROR:", err)
}
} else {
yamlFile, err := os.ReadFile(rulesUrl)
if err != nil {
log.Println("ERROR:", err)
}
yaml.Unmarshal(yamlFile, &ruleSet)
}
domains := []string{}
for _, rule := range ruleSet {
domains = append(domains, rule.Domain)
domains = append(domains, rule.Domains...)
if os.Getenv("ALLOWED_DOMAINS_RULESET") == "true" {
allowedDomains = append(allowedDomains, domains...)
}
}
log.Println("Loaded ", len(ruleSet), " rules for", len(domains), "Domains")
return ruleSet
}
func fetchRule(domain string, path string) Rule {
if len(rulesSet) == 0 {
return Rule{}
}
rule := Rule{}
for _, rule := range rulesSet {
domains := rule.Domains
if rule.Domain != "" {
domains = append(domains, rule.Domain)
}
for _, ruleDomain := range domains {
if ruleDomain == domain || strings.HasSuffix(domain, ruleDomain) {
if len(rule.Paths) > 0 && !StringInSlice(path, rule.Paths) {
continue
}
// return first match
return rule
}
}
}
return rule
}
func applyRules(body string, rule Rule) string {
if len(rulesSet) == 0 {
return body
}
for _, regexRule := range rule.RegexRules {
re := regexp.MustCompile(regexRule.Match)
body = re.ReplaceAllString(body, regexRule.Replace)
}
for _, injection := range rule.Injections {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
if err != nil {
log.Fatal(err)
}
if injection.Replace != "" {
doc.Find(injection.Position).ReplaceWithHtml(injection.Replace)
}
if injection.Append != "" {
doc.Find(injection.Position).AppendHtml(injection.Append)
}
if injection.Prepend != "" {
doc.Find(injection.Position).PrependHtml(injection.Prepend)
}
body, err = doc.Html()
if err != nil {
log.Fatal(err)
}
}
return body
}
func StringInSlice(s string, list []string) bool {
for _, x := range list {
if strings.HasPrefix(s, x) {
return true
}
}
return false
} }

View File

@@ -1,58 +0,0 @@
// BEGIN: 6f8b3f5d5d5d
package handlers
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
func TestProxySite(t *testing.T) {
app := fiber.New()
app.Get("/:url", ProxySite)
req := httptest.NewRequest("GET", "/https://example.com", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestRewriteHtml(t *testing.T) {
bodyB := []byte(`
<html>
<head>
<title>Test Page</title>
</head>
<body>
<img src="/image.jpg">
<script src="/script.js"></script>
<a href="/about">About Us</a>
<div style="background-image: url('/background.jpg')"></div>
</body>
</html>
`)
u := &url.URL{Host: "example.com"}
expected := `
<html>
<head>
<title>Test Page</title>
</head>
<body>
<img src="/https://example.com/image.jpg">
<script script="/https://example.com/script.js"></script>
<a href="/https://example.com/about">About Us</a>
<div style="background-image: url('/https://example.com/background.jpg')"></div>
</body>
</html>
`
actual := rewriteHtml(bodyB, u, Rule{})
assert.Equal(t, expected, actual)
}
// END: 6f8b3f5d5d5d

View File

@@ -1,21 +0,0 @@
package handlers
import (
"log"
"github.com/gofiber/fiber/v2"
)
func Raw(c *fiber.Ctx) error {
// Get the url from the URL
urlQuery := c.Params("*")
queries := c.Queries()
body, _, _, err := fetchSite(urlQuery, queries)
if err != nil {
log.Println("ERROR:", err)
c.SendStatus(500)
return c.SendString(err.Error())
}
return c.SendString(body)
}

View File

@@ -1,60 +0,0 @@
// BEGIN: 7f8d9e6d4b5c
package handlers
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestRaw(t *testing.T) {
app := fiber.New()
app.Get("/raw/*", Raw)
testCases := []struct {
name string
url string
expected string
}{
{
name: "valid url",
url: "https://www.google.com",
expected: "<!doctype html>",
},
{
name: "invalid url",
url: "invalid-url",
expected: "parse invalid-url: invalid URI for request",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/raw/"+tc.url, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status OK; got %v", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(string(body), tc.expected) {
t.Errorf("expected body to contain %q; got %q", tc.expected, string(body))
}
})
}
}
// END: 7f8d9e6d4b5c

View File

@@ -1,23 +0,0 @@
package handlers
import (
"os"
"github.com/gofiber/fiber/v2"
"gopkg.in/yaml.v3"
)
func Ruleset(c *fiber.Ctx) error {
if os.Getenv("EXPOSE_RULESET") == "false" {
c.SendStatus(fiber.StatusForbidden)
return c.SendString("Rules Disabled")
}
body, err := yaml.Marshal(rulesSet)
if err != nil {
c.SendStatus(fiber.StatusInternalServerError)
return c.SendString(err.Error())
}
return c.SendString(string(body))
}

33
handlers/script.go Normal file
View File

@@ -0,0 +1,33 @@
package handlers
import (
"embed"
"github.com/gofiber/fiber/v2"
)
//go:embed script.js
var scriptData embed.FS
//go:embed playground-script.js
var playgroundScriptData embed.FS
func Script(c *fiber.Ctx) error {
if c.Path() == "/script.js" {
scriptData, err := scriptData.ReadFile("script.js")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
c.Set("Content-Type", "text/javascript")
return c.Send(scriptData)
}
if c.Path() == "/playground-script.js" {
playgroundScriptData, err := playgroundScriptData.ReadFile("playground-script.js")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
c.Set("Content-Type", "text/javascript")
return c.Send(playgroundScriptData)
}
return nil
}

330
handlers/script.js Normal file
View File

@@ -0,0 +1,330 @@
const labels = document.querySelectorAll("label");
const inputs = document.querySelectorAll('input[type="radio"]');
const mainElement = document.querySelector("main");
const inputField = document.getElementById("inputField");
const clearButton = document.getElementById("clearButton");
window.addEventListener("DOMContentLoaded", handleDOMContentLoaded);
function handleDOMContentLoaded() {
handleFontChange();
handleFontSizeChange();
inputs.forEach((input) => {
const storedValue = localStorage.getItem(input.name);
if (storedValue === input.value) {
input.checked = true;
}
});
window.removeEventListener("DOMContentLoaded", handleDOMContentLoaded);
}
function clearInput() {
inputField.value = "";
clearButton.style.display = "none";
inputField.focus();
}
if (inputField !== null && clearButton !== null) {
inputField.addEventListener("input", () => {
const clearButton = document.getElementById("clearButton");
if (clearButton !== null) {
if (inputField.value.trim().length > 0) {
clearButton.style.display = "block";
} else {
clearButton.style.display = "none";
}
}
});
inputField.addEventListener("keydown", (event) => {
if (event.code === "Escape") {
clearInput();
}
});
clearButton.addEventListener("click", () => {
clearInput();
});
}
function focusable_children(node) {
const nodes = Array.from(
node.querySelectorAll(
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
)
).filter((s) => s.offsetParent !== null);
const index = nodes.indexOf(document.activeElement);
const update = (d) => {
let i = index + d;
i += nodes.length;
i %= nodes.length;
nodes[i].focus();
};
return {
next: (selector) => {
const reordered = [
...nodes.slice(index + 1),
...nodes.slice(0, index + 1),
];
for (let i = 0; i < reordered.length; i += 1) {
if (!selector || reordered[i].matches(selector)) {
reordered[i].focus();
return;
}
}
},
prev: (selector) => {
const reordered = [
...nodes.slice(index + 1),
...nodes.slice(0, index + 1),
];
for (let i = reordered.length - 2; i >= 0; i -= 1) {
if (!selector || reordered[i].matches(selector)) {
reordered[i].focus();
return;
}
}
},
update,
};
}
function trap(node) {
const handle_keydown = (e) => {
if (e.key === "Tab") {
e.preventDefault();
const group = focusable_children(node);
if (e.shiftKey) {
group.prev();
} else {
group.next();
}
}
};
node.addEventListener("keydown", handle_keydown);
return {
destroy: () => {
node.removeEventListener("keydown", handle_keydown);
},
};
}
const toggleDropdown = () => {
const dropdown = document.getElementById("dropdown");
const dropdown_button = dropdown.querySelector("button");
const dropdown_panel = document.getElementById("dropdown_panel");
const focusTrap = trap(dropdown);
const closeDropdown = () => {
dropdown_panel.classList.add("hidden");
dropdown_button.setAttribute("aria-expanded", "false");
focusTrap.destroy();
dropdown.removeEventListener("keydown", handleEscapeKey);
document.removeEventListener("click", handleClickOutside);
inputs.forEach((input) => {
input.removeEventListener("change", handleInputChange);
});
labels.forEach((label) => {
label.removeEventListener("click", handleLabelSelection);
});
};
const handleClickOutside = (e) => {
if (dropdown !== null && !dropdown.contains(e.target)) {
closeDropdown();
}
};
const handleEscapeKey = (e) => {
if (e.key === "Escape") {
dropdown_panel.classList.add("hidden");
closeDropdown();
}
};
const handleInputChange = (e) => {
if (e.target.checked) {
localStorage.setItem(e.target.name, e.target.value);
switch (e.target.name) {
case "theme": {
handleThemeChange();
break;
}
case "font": {
handleFontChange();
break;
}
case "fontsize": {
handleFontSizeChange();
break;
}
default: {
console.error("Unknown event");
break;
}
}
}
};
const handleLabelSelection = (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const input = document.getElementById(e.target.getAttribute("for"));
input.checked = true;
input.dispatchEvent(new Event("change", { bubbles: true }));
}
};
if (dropdown_panel.classList.contains("hidden")) {
dropdown_panel.classList.remove("hidden");
dropdown_button.setAttribute("aria-expanded", "true");
dropdown.addEventListener("keydown", handleEscapeKey);
inputs.forEach((input) => {
input.addEventListener("change", handleInputChange);
});
labels.forEach((label) => {
label.addEventListener("keydown", handleLabelSelection);
});
document.addEventListener("click", handleClickOutside);
} else {
closeDropdown();
}
};
const handleFontChange = () => {
if (mainElement === null) {
return;
}
let font = localStorage.getItem("font");
if (font === null) {
localStorage.setItem("font", "sans-serif");
font = "sans-serif";
}
if (font === "serif") {
mainElement.classList.add("font-serif");
mainElement.classList.remove("font-sans");
} else {
mainElement.classList.add("font-sans");
mainElement.classList.remove("font-serif");
}
};
const changeFontSize = (node, classes) => {
const sizes = [
"text-xs",
"text-sm",
"text-base",
"text-lg",
"text-xl",
"text-2xl",
"text-3xl",
"text-4xl",
"text-5xl",
"lg:text-4xl",
"lg:text-5xl",
"lg:text-6xl",
];
const currentClasses = sizes.filter((size) => node.classList.contains(size));
node.classList.remove(...currentClasses);
node.classList.add(...classes);
};
const handleFontSizeChange = () => {
if (mainElement === null) {
return;
}
let fontSize = localStorage.getItem("fontsize");
if (fontSize === null) {
localStorage.setItem("fontsize", "text-base");
fontSize = "text-base";
}
if (fontSize === "text-sm") {
changeFontSize(document.querySelector("body"), ["text-sm"]);
} else if (fontSize === "text-lg") {
changeFontSize(document.querySelector("body"), ["text-lg"]);
} else {
changeFontSize(document.querySelector("body"), ["text-base"]);
}
const nodes = document.querySelectorAll(
"h1, h2, h3, h4, h5, h6, code, pre, kbd, table"
);
if (fontSize === "text-sm") {
changeFontSize(mainElement, ["text-sm"]);
} else if (fontSize === "text-lg") {
changeFontSize(mainElement, ["text-lg"]);
} else {
changeFontSize(mainElement, ["text-base"]);
}
nodes.forEach((node) => {
let classes = "";
switch (node.tagName) {
case "H1": {
if (fontSize === "text-sm") {
classes = ["text-3xl", "lg:text-4xl"];
} else if (fontSize === "text-lg") {
classes = ["text-5xl", "lg:text-6xl"];
} else {
classes = ["text-4xl", "lg:text-5xl"];
}
break;
}
case "H2": {
if (fontSize === "text-sm") {
classes = ["text-2xl"];
} else if (fontSize === "text-lg") {
classes = ["text-4xl"];
} else {
classes = ["text-3xl"];
}
break;
}
case "H3": {
if (fontSize === "text-sm") {
classes = ["text-xl"];
} else if (fontSize === "text-lg") {
classes = ["text-3xl"];
} else {
classes = ["text-2xl"];
}
break;
}
case "H4":
case "H5":
case "H6": {
if (fontSize === "text-sm") {
classes = ["text-lg"];
} else if (fontSize === "text-lg") {
classes = ["text-2xl"];
} else {
classes = ["text-xl"];
}
break;
}
case "CODE":
case "PRE":
case "KBD":
case "TABLE": {
if (fontSize === "text-sm") {
classes = ["text-xs"];
} else if (fontSize === "text-lg") {
classes = ["text-base"];
} else {
classes = ["text-sm"];
}
break;
}
default: {
if (fontSize === "text-sm") {
classes = ["text-sm"];
} else if (fontSize === "text-lg") {
classes = ["text-lg"];
} else {
classes = ["text-base"];
}
break;
}
}
changeFontSize(node, classes);
});
};

1
handlers/styles.css Normal file

File diff suppressed because one or more lines are too long

23
handlers/styles.go Normal file
View File

@@ -0,0 +1,23 @@
package handlers
import (
"embed"
"github.com/gofiber/fiber/v2"
)
//go:embed styles.css
var cssData embed.FS
func Styles(c *fiber.Ctx) error {
cssData, err := cssData.ReadFile("styles.css")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
c.Set("Content-Type", "text/css")
return c.Send(cssData)
}

View File

@@ -1,40 +0,0 @@
package handlers
type Regex struct {
Match string `yaml:"match"`
Replace string `yaml:"replace"`
}
type KV struct {
Key string `yaml:"key"`
Value string `yaml:"value"`
}
type RuleSet []Rule
type Rule struct {
Domain string `yaml:"domain,omitempty"`
Domains []string `yaml:"domains,omitempty"`
Paths []string `yaml:"paths,omitempty"`
Headers struct {
UserAgent string `yaml:"user-agent,omitempty"`
XForwardedFor string `yaml:"x-forwarded-for,omitempty"`
Referer string `yaml:"referer,omitempty"`
Cookie string `yaml:"cookie,omitempty"`
CSP string `yaml:"content-security-policy,omitempty"`
} `yaml:"headers,omitempty"`
GoogleCache bool `yaml:"googleCache,omitempty"`
RegexRules []Regex `yaml:"regexRules"`
UrlMods struct {
Domain []Regex `yaml:"domain"`
Path []Regex `yaml:"path"`
Query []KV `yaml:"query"`
} `yaml:"urlMods"`
Injections []struct {
Position string `yaml:"position"`
Append string `yaml:"append"`
Prepend string `yaml:"prepend"`
Replace string `yaml:"replace"`
} `yaml:"injections"`
}

82
internal/cli/art.go Normal file
View File

@@ -0,0 +1,82 @@
package cli
import (
"fmt"
"os"
"strings"
"golang.org/x/term"
)
var art string = `
_____╬═╬____________________________________________
|_|__╬═╬___|___|___|___| EVERYWALL |___|___|___|___|
|___|╬═╬|___▄▄▌ ▄▄▄· ·▄▄▄▄ ·▄▄▄▄ ▄▄▄ .▄▄▄ __|_|
|_|__╬═╬___|██• ▐█ ▀█ ██▪ ██ ██▪ ██ ▀▄.▀·▀▄ █·|___|
|___|╬═╬|___██▪ ▄█▀▀█ ▐█· ▐█▌▐█· ▐█▌▐▀▀▪▄▐▀▀▄ __|_|
|_|__╬═╬___|▐█▌▐▌▐█ ▪▐▌██. ██ ██. ██ ▐█▄▄▌▐█•█▌|___|
|___|╬═╬|___.▀▀▀ ▀ ▀ ▀▀▀▀▀• ▀▀▀▀▀• ▀▀▀ .▀ ▀__|_|
|_|__╬═╬___|___|___|_ VERSION %-7s__|___|___|___|
|___|╬═╬|____|___|___|___|___|___|___|___|___|___|_|
╬═╬
╬═╬ %s
`
func StartupMessage(version string, port string, ruleset string) string {
isTerm := term.IsTerminal(int(os.Stdout.Fd()))
version = strings.Trim(version, " ")
version = strings.Trim(version, "\n")
var link string
if isTerm {
link = createHyperlink("http://localhost:" + port)
} else {
link = "http://localhost:" + port
}
buf := fmt.Sprintf(art, version, link)
if isTerm {
buf = blinkChars(buf, '.', '•', '·', '▪')
}
if ruleset == "" {
buf += "\n [!] no ruleset specified.\n [!] for better performance, use a ruleset using --ruleset\n"
}
if isTerm {
buf = colorizeNonASCII(buf)
}
return buf
}
func createHyperlink(url string) string {
return fmt.Sprintf("\033[4m%s\033[0m", url)
}
func colorizeNonASCII(input string) string {
result := ""
for _, r := range input {
if r > 127 {
// If the character is non-ASCII, color it blue
result += fmt.Sprintf("\033[34m%c\033[0m", r)
} else {
// ASCII characters remain unchanged
result += string(r)
}
}
return result
}
func blinkChars(input string, chars ...rune) string {
result := ""
MAIN:
for _, x := range input {
for _, y := range chars {
if x == y {
result += fmt.Sprintf("\033[5m%s\033[0m", string(x))
continue MAIN
}
}
result += fmt.Sprintf("%s", string(x))
}
return result
}

View File

@@ -0,0 +1,71 @@
package cli
import (
"fmt"
"io"
"os"
ruleset_v2 "github.com/everywall/ladder/proxychain/ruleset"
)
// HandleRulesetMerge merges a set of ruleset files, specified by the rulesetPath or RULESET env variable, into either YAML or Gzip format.
// Exits the program with an error message if the ruleset path is not provided or if loading the ruleset fails.
//
// Parameters:
// - rulesetPath: Specifies the path to the ruleset file.
// - mergeRulesets: Indicates if a merge operation should be performed.
// - useGzip: Indicates if the merged rulesets should be gzip-ped.
// - output: Specifies the output file. If nil, stdout will be used.
//
// Returns:
// - An error if the ruleset loading or merging process fails, otherwise nil.
func HandleRulesetMerge(rulesetPath string, mergeRulesets bool, output *os.File) error {
if !mergeRulesets {
return nil
}
if rulesetPath == "" {
rulesetPath = os.Getenv("RULESET")
}
if rulesetPath == "" {
fmt.Println("error: no ruleset provided. Try again with --ruleset <ruleset.yaml>")
os.Exit(1)
}
rs, err := ruleset_v2.NewRuleset(rulesetPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return yamlMerge(rs, output)
}
// yamlMerge takes a RuleSet and an optional output file path pointer. It converts the RuleSet into YAML format.
// If the output file path is provided, the YAML data is written to this file. If not, the YAML data is printed to stdout.
//
// Parameters:
// - rs: The ruleset.RuleSet to be converted to YAML.
// - output: The output for the merged data. If nil, stdout will be used.
//
// Returns:
// - An error if YAML conversion or file writing fails, otherwise nil.
func yamlMerge(rs ruleset_v2.Ruleset, output io.Writer) error {
yaml, err := rs.YAML()
if err != nil {
return err
}
if output == nil {
fmt.Println(yaml)
os.Exit(0)
}
_, err = io.WriteString(output, yaml)
if err != nil {
return fmt.Errorf("failed to write merged YAML ruleset: %v", err)
}
return nil
}

View File

@@ -1,9 +1,9 @@
{ {
"scripts": { "scripts": {
"build": "pnpx tailwindcss -i ./styles/input.css -o ./styles/output.css --build && pnpx minify ./styles/output.css > ./cmd/styles.css" "build": "pnpx tailwindcss -i ./styles/input.css -o ./styles/output.css --build --minify",
"watch": "pnpx tailwindcss -i ./styles/input.css -o ./styles/output.css --build --minify --watch"
}, },
"devDependencies": { "devDependencies": {
"minify": "^10.5.2", "tailwindcss": "^3.3.6"
"tailwindcss": "^3.3.5"
} }
} }

View File

@@ -0,0 +1,16 @@
## TLDR
- If you create, delete or rename any request/response modifier, run `go run codegen.go`, so that ruleset unmarshaling will work properly.
## Overview
The `codegen.go` file is a utility for the rulesets that automatically generates Go code that maps functional options names found in response/request modifiers to corresponding factory functions. This generation is crucial for the serialization of rulesets from JSON or YAML into functional options suitable for use in proxychains. The tool processes Go files containing modifier functions and generates the necessary mappings.
- The generated mappings will be written in `proxychain/ruleset/rule_reqmod_types.gen.go` and `proxychain/ruleset/rule_resmod_types.gen.go`.
- These files are used in UnmarshalJSON and UnmarshalYAML methods of the rule type, found in `proxychain/ruleset/rule.go`
## Usage
```sh
go run codegen.go
```

View File

@@ -0,0 +1,205 @@
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/fs"
//"io/fs"
"os"
"path/filepath"
"strings"
//"strings"
)
func responseModToFactoryMap(fn *ast.FuncDecl) (modMap string) {
paramCount := len(fn.Type.Params.List)
name := fn.Name.Name
var x string
switch paramCount {
case 0:
x = fmt.Sprintf(" rsmModMap[\"%s\"] = func(_ ...string) proxychain.ResponseModification {\n return tx.%s()\n }\n", name, name)
default:
p := []string{}
for i := 0; i < paramCount; i++ {
p = append(p, fmt.Sprintf("params[%d]", i))
}
params := strings.Join(p, ", ")
x = fmt.Sprintf(" rsmModMap[\"%s\"] = func(params ...string) proxychain.ResponseModification {\n return tx.%s(%s)\n }\n", name, name, params)
}
return x
}
func responseModCodeGen(dir string) (code string, err error) {
fset := token.NewFileSet()
files, err := os.ReadDir(dir)
if err != nil {
panic(err)
}
factoryMaps := []string{}
for _, file := range files {
if !shouldGenCodeFor(file) {
continue
}
// Parse each Go file
node, err := parser.ParseFile(fset, filepath.Join(dir, file.Name()), nil, parser.ParseComments)
if err != nil {
return "", err
}
ast.Inspect(node, func(n ast.Node) bool {
fn, ok := n.(*ast.FuncDecl)
if ok && fn.Recv == nil && fn.Name.IsExported() {
factoryMaps = append(factoryMaps, responseModToFactoryMap(fn))
}
return true
})
}
code = fmt.Sprintf(`
package ruleset_v2
// DO NOT EDIT THIS FILE. It is automatically generated by ladder/proxychain/codegen/codegen.go
// The purpose of this is serialization of rulesets from JSON or YAML into functional options suitable
// for use in proxychains.
import (
"github.com/everywall/ladder/proxychain"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
)
type ResponseModifierFactory func(params ...string) proxychain.ResponseModification
var rsmModMap map[string]ResponseModifierFactory
func init() {
rsmModMap = make(map[string]ResponseModifierFactory)
%s
}`, strings.Join(factoryMaps, "\n"))
// fmt.Println(code)
return code, nil
}
func requestModToFactoryMap(fn *ast.FuncDecl) (modMap string) {
paramCount := len(fn.Type.Params.List)
name := fn.Name.Name
var x string
switch paramCount {
case 0:
x = fmt.Sprintf(" rqmModMap[\"%s\"] = func(_ ...string) proxychain.RequestModification {\n return rx.%s()\n }\n", name, name)
default:
p := []string{}
for i := 0; i < paramCount; i++ {
p = append(p, fmt.Sprintf("params[%d]", i))
}
params := strings.Join(p, ", ")
x = fmt.Sprintf(" rqmModMap[\"%s\"] = func(params ...string) proxychain.RequestModification {\n return rx.%s(%s)\n }\n", name, name, params)
}
return x
}
func requestModCodeGen(dir string) (code string, err error) {
fset := token.NewFileSet()
files, err := os.ReadDir(dir)
if err != nil {
panic(err)
}
factoryMaps := []string{}
for _, file := range files {
if !shouldGenCodeFor(file) {
continue
}
// Parse each Go file
node, err := parser.ParseFile(fset, filepath.Join(dir, file.Name()), nil, parser.ParseComments)
if err != nil {
return "", err
}
ast.Inspect(node, func(n ast.Node) bool {
fn, ok := n.(*ast.FuncDecl)
if ok && fn.Recv == nil && fn.Name.IsExported() {
factoryMaps = append(factoryMaps, requestModToFactoryMap(fn))
}
return true
})
}
code = fmt.Sprintf(`
package ruleset_v2
// DO NOT EDIT THIS FILE. It is automatically generated by ladder/proxychain/codegen/codegen.go
// The purpose of this is serialization of rulesets from JSON or YAML into functional options suitable
// for use in proxychains.
import (
"github.com/everywall/ladder/proxychain"
rx "github.com/everywall/ladder/proxychain/requestmodifiers"
)
type RequestModifierFactory func(params ...string) proxychain.RequestModification
var rqmModMap map[string]RequestModifierFactory
func init() {
rqmModMap = make(map[string]RequestModifierFactory)
%s
}`, strings.Join(factoryMaps, "\n"))
// fmt.Println(code)
return code, nil
}
func shouldGenCodeFor(file fs.DirEntry) bool {
if file.IsDir() {
return false
}
if filepath.Ext(file.Name()) != ".go" {
return false
}
if strings.HasSuffix(file.Name(), "_test.go") {
return false
}
return true
}
func main() {
rqmCode, err := requestModCodeGen("../requestmodifiers/")
if err != nil {
panic(err)
}
// fmt.Println(rqmCode)
fq, err := os.Create("../ruleset/rule_reqmod_types.gen.go")
if err != nil {
panic(err)
}
_, err = io.WriteString(fq, rqmCode)
if err != nil {
panic(err)
}
rsmCode, err := responseModCodeGen("../responsemodifiers/")
if err != nil {
panic(err)
}
// fmt.Println(rsmCode)
fs, err := os.Create("../ruleset/rule_resmod_types.gen.go")
if err != nil {
panic(err)
}
_, err = io.WriteString(fs, rsmCode)
if err != nil {
panic(err)
}
}

555
proxychain/proxychain.go Normal file
View File

@@ -0,0 +1,555 @@
package proxychain
import (
"errors"
"fmt"
"io"
"log"
"net/url"
"strings"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
profiles "github.com/bogdanfinn/tls-client/profiles"
"github.com/gofiber/fiber/v2"
)
/*
ProxyChain manages the process of forwarding an HTTP request to an upstream server,
applying request and response modifications along the way.
- It accepts incoming HTTP requests (as a Fiber *ctx), and applies
request modifiers (ReqMods) and response modifiers (ResMods) before passing the
upstream response back to the client.
- ProxyChains can be reused to avoid memory allocations. However, they are not concurrent-safe
so a ProxyChainPool should be used with mutexes to avoid memory errors.
---
# EXAMPLE
```
import (
rx "ladder/pkg/proxychain/requestmodifiers"
tx "ladder/pkg/proxychain/responsemodifiers"
"ladder/pkg/proxychain/responsemodifiers/rewriters"
"ladder/internal/proxychain"
)
proxychain.NewProxyChain().
SetFiberCtx(c).
SetRequestModifications(
rx.BlockOutgoingCookies(),
rx.SpoofOrigin(),
rx.SpoofReferrer(),
).
SetResultModifications(
tx.BlockIncomingCookies(),
tx.RewriteHTMLResourceURLs()
).
Execute()
```
client ladder service upstream
┌─────────┐ ┌────────────────────────┐ ┌─────────┐
│ │GET │ │ │ │
│ req────┼───► ProxyChain │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ apply │ │ │
│ │ │ RequestModifications │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ send GET │ │ │
│ │ │ Request req────────┼─► │ │
│ │ │ │ │ │
│ │ │ 200 OK │ │ │
│ │ │ ┌────────────────┼─response │
│ │ │ ▼ │ │ │
│ │ │ apply │ │ │
│ │ │ ResultModifications │ │ │
│ │ │ │ │ │ │
│ │◄───┼───────┘ │ │ │
│ │ │ 200 OK │ │ │
│ │ │ │ │ │
└─────────┘ └────────────────────────┘ └─────────┘
*/
type ProxyChain struct {
Context *fiber.Ctx
Client HTTPClient
onceClient HTTPClient
Request *http.Request
Response *http.Response
requestModifications []RequestModification
onceRequestModifications []RequestModification
onceResponseModifications []ResponseModification
responseModifications []ResponseModification
debugMode bool
abortErr error
APIPrefix string
}
// a ProxyStrategy is a pre-built proxychain with purpose-built defaults
type ProxyStrategy ProxyChain
// A RequestModification is a function that should operate on the
// ProxyChain Req or Client field, using the fiber ctx as needed.
type RequestModification func(*ProxyChain) error
// A ResponseModification is a function that should operate on the
// ProxyChain Res (http result) & Body (buffered http response body) field
type ResponseModification func(*ProxyChain) error
// abstraction over HTTPClient
type HTTPClient interface {
GetCookies(u *url.URL) []*http.Cookie
SetCookies(u *url.URL, cookies []*http.Cookie)
SetCookieJar(jar http.CookieJar)
GetCookieJar() http.CookieJar
SetProxy(proxyURL string) error
GetProxy() string
SetFollowRedirect(followRedirect bool)
GetFollowRedirect() bool
CloseIdleConnections()
Do(req *http.Request) (*http.Response, error)
Get(url string) (resp *http.Response, err error)
Head(url string) (resp *http.Response, err error)
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
}
// SetRequestModifications sets the ProxyChain's request modifiers
// the modifier will not fire until ProxyChain.Execute() is run.
func (chain *ProxyChain) SetRequestModifications(mods ...RequestModification) *ProxyChain {
chain.requestModifications = mods
return chain
}
// AddRequestModifications adds more request modifiers to the ProxyChain
// the modifier will not fire until ProxyChain.Execute() is run.
func (chain *ProxyChain) AddRequestModifications(mods ...RequestModification) *ProxyChain {
chain.requestModifications = append(chain.requestModifications, mods...)
return chain
}
// AddOnceRequestModifications adds a request modifier to the ProxyChain that should only fire once
// the modifier will not fire until ProxyChain.Execute() is run and will be removed after it has been applied.
func (chain *ProxyChain) AddOnceRequestModifications(mods ...RequestModification) *ProxyChain {
chain.onceRequestModifications = append(chain.onceRequestModifications, mods...)
return chain
}
// AddOnceResponseModifications adds a response modifier to the ProxyChain that should only fire once
// the modifier will not fire until ProxyChain.Execute() is run and will be removed after it has been applied.
func (chain *ProxyChain) AddOnceResponseModifications(mods ...ResponseModification) *ProxyChain {
chain.onceResponseModifications = append(chain.onceResponseModifications, mods...)
return chain
}
// AddResponseModifications sets the ProxyChain's response modifiers
// the modifier will not fire until ProxyChain.Execute() is run.
func (chain *ProxyChain) AddResponseModifications(mods ...ResponseModification) *ProxyChain {
chain.responseModifications = append(chain.responseModifications, mods...)
return chain
}
// SetResponseModifications sets the ProxyChain's response modifiers
// the modifier will not fire until ProxyChain.Execute() is run.
func (chain *ProxyChain) SetResponseModifications(mods ...ResponseModification) *ProxyChain {
chain.responseModifications = mods
return chain
}
// WithAPIPath trims the path during URL extraction.
// example: using path = "api/outline/", a path like "http://localhost:8080/api/outline/https://example.com" becomes "https://example.com"
func (chain *ProxyChain) WithAPIPath(path string) *ProxyChain {
chain.APIPrefix = path
chain.APIPrefix = strings.TrimSuffix(chain.APIPrefix, "*")
return chain
}
func (chain *ProxyChain) _initializeRequest() (*http.Request, error) {
if chain.Context == nil {
chain.abortErr = chain.abort(errors.New("no context set"))
return nil, chain.abortErr
}
// initialize a request (without url)
req, err := http.NewRequest(chain.Context.Method(), "", nil)
if err != nil {
return nil, err
}
chain.Request = req
switch chain.Context.Method() {
case "GET":
case "DELETE":
case "HEAD":
case "OPTIONS":
break
case "POST":
case "PUT":
case "PATCH":
// stream content of body from client request to upstream request
chain.Request.Body = io.NopCloser(chain.Context.Request().BodyStream())
default:
return nil, fmt.Errorf("unsupported request method from client: '%s'", chain.Context.Method())
}
return req, nil
}
// reconstructURLFromReferer reconstructs the URL using the referer's scheme, host, and the relative path / queries
func reconstructURLFromReferer(referer *url.URL, relativeURL *url.URL) (*url.URL, error) {
// Extract the real url from referer path
realURL, err := url.Parse(strings.TrimPrefix(referer.Path, "/"))
if err != nil {
return nil, fmt.Errorf("error parsing real URL from referer '%s': %v", referer.Path, err)
}
if realURL.Scheme == "" || realURL.Host == "" {
return nil, fmt.Errorf("invalid referer URL: '%s' on request '%s", referer.String(), relativeURL.String())
}
log.Printf("rewrite relative URL using referer: '%s' -> '%s'\n", relativeURL.String(), realURL.String())
return &url.URL{
Scheme: referer.Scheme,
Host: referer.Host,
Path: realURL.Path,
RawQuery: realURL.RawQuery,
}, nil
}
// prevents calls like: http://localhost:8080/http://localhost:8080
func preventRecursiveProxyRequest(urlQuery *url.URL, baseProxyURL string) *url.URL {
u := urlQuery.String()
isRecursive := strings.HasPrefix(u, baseProxyURL) || u == baseProxyURL
if !isRecursive {
return urlQuery
}
fixedURL, err := url.Parse(strings.TrimPrefix(strings.TrimPrefix(urlQuery.String(), baseProxyURL), "/"))
if err != nil {
log.Printf("proxychain: failed to fix recursive request: '%s' -> '%s\n'", baseProxyURL, u)
return urlQuery
}
return preventRecursiveProxyRequest(fixedURL, baseProxyURL)
}
// extractURL extracts a URL from the request ctx
func (chain *ProxyChain) extractURL() (*url.URL, error) {
isLocal := strings.HasPrefix(chain.Context.BaseURL(), "http://localhost") || strings.HasPrefix(chain.Context.BaseURL(), "http://127.0.0.1")
isReqPath := strings.HasPrefix(chain.Context.Path(), "/http")
isAPI := strings.HasPrefix(chain.Context.Path(), "/api")
isOutline := strings.HasPrefix(chain.Context.Path(), "/outline")
if isLocal || isReqPath || isAPI || isOutline {
return chain.extractURLFromPath()
}
u, err := url.Parse(chain.Context.BaseURL())
if err != nil {
return &url.URL{}, err
}
parts := strings.Split(u.Hostname(), ".")
if len(parts) < 2 {
fmt.Println("path")
return chain.extractURLFromPath()
}
return chain.extractURLFromSubdomain()
}
// extractURLFromPath extracts a URL from the request ctx if subdomains are used.
func (chain *ProxyChain) extractURLFromSubdomain() (*url.URL, error) {
u, err := url.Parse(chain.Context.BaseURL())
if err != nil {
return &url.URL{}, err
}
parts := strings.Split(u.Hostname(), ".")
if len(parts) < 2 {
// no subdomain set, fallback to path extraction
//panic("asdf")
return chain.extractURLFromPath()
}
subdomain := strings.Join(parts[:len(parts)-2], ".")
subURL := subdomain
subURL = strings.ReplaceAll(subURL, "--", "|")
subURL = strings.ReplaceAll(subURL, "-", ".")
subURL = strings.ReplaceAll(subURL, "|", "-")
return url.Parse(fmt.Sprintf("https://%s/%s", subURL, u.Path))
}
// extractURLFromPath extracts a URL from the request ctx. If the URL in the request
// is a relative path, it reconstructs the full URL using the referer header.
func (chain *ProxyChain) extractURLFromPath() (*url.URL, error) {
reqURL := chain.Context.Params("*")
reqURL = strings.TrimPrefix(reqURL, chain.APIPrefix)
// sometimes client requests doubleroot '//'
// there is a bug somewhere else, but this is a workaround until we find it
if strings.HasPrefix(reqURL, "/") || strings.HasPrefix(reqURL, `%2F`) {
reqURL = strings.TrimPrefix(reqURL, "/")
reqURL = strings.TrimPrefix(reqURL, `%2F`)
}
// unescape url query
uReqURL, err := url.QueryUnescape(reqURL)
if err == nil {
reqURL = uReqURL
}
urlQuery, err := url.Parse(reqURL)
if err != nil {
return nil, fmt.Errorf("error parsing request URL '%s': %v", reqURL, err)
}
// prevent recursive proxy requests
fullURL := chain.Context.Request().URI()
proxyURL := fmt.Sprintf("%s://%s", fullURL.Scheme(), fullURL.Host())
urlQuery = preventRecursiveProxyRequest(urlQuery, proxyURL)
// Handle standard paths
// eg: https://localhost:8080/https://realsite.com/images/foobar.jpg -> https://realsite.com/images/foobar.jpg
isRelativePath := urlQuery.Scheme == ""
if !isRelativePath {
return urlQuery, nil
}
// Handle relative URLs
// eg: https://localhost:8080/images/foobar.jpg -> https://realsite.com/images/foobar.jpg
referer, err := url.Parse(chain.Context.Get("referer"))
relativePath := urlQuery
if err != nil {
return nil, fmt.Errorf("error parsing referer URL from req: '%s': %v", relativePath, err)
}
return reconstructURLFromReferer(referer, relativePath)
}
// SetFiberCtx takes the request ctx from the client
// for the modifiers and execute function to use.
// it must be set everytime a new request comes through
// if the upstream request url cannot be extracted from the ctx,
// a 500 error will be sent back to the client
func (chain *ProxyChain) SetFiberCtx(ctx *fiber.Ctx) *ProxyChain {
chain.Context = ctx
// initialize the request and prepare it for modification
req, err := chain._initializeRequest()
if err != nil {
chain.abortErr = chain.abort(err)
}
chain.Request = req
// extract the URL for the request and add it to the new request
url, err := chain.extractURL()
if err != nil {
chain.abortErr = chain.abort(err)
} else {
chain.Request.URL = url
fmt.Printf("extracted URL: %s\n", chain.Request.URL)
}
return chain
}
func (chain *ProxyChain) validateCtxIsSet() error {
if chain.Context != nil {
return nil
}
err := errors.New("proxyChain was called without setting a fiber Ctx. Use ProxyChain.SetFiberCtx()")
chain.abortErr = chain.abort(err)
return chain.abortErr
}
// SetHTTPClient sets a new upstream http client transport
// useful for modifying TLS
func (chain *ProxyChain) SetHTTPClient(httpClient HTTPClient) *ProxyChain {
chain.Client = httpClient
return chain
}
// SetOnceHTTPClient sets a new upstream http client transport temporarily
// and clears it once it is used.
func (chain *ProxyChain) SetOnceHTTPClient(httpClient HTTPClient) *ProxyChain {
chain.onceClient = httpClient
return chain
}
// SetVerbose changes the logging behavior to print
// the modification steps and applied rulesets for debugging
func (chain *ProxyChain) SetDebugLogging(isDebugMode bool) *ProxyChain {
if isDebugMode {
log.Println("DEBUG MODE ENABLED")
}
chain.debugMode = isDebugMode
return chain
}
// abort proxychain and return 500 error to client
// this will prevent Execute from firing and reset the state
// returns the initial error enriched with context
func (chain *ProxyChain) abort(err error) error {
// defer chain._reset()
chain.abortErr = err
// chain.Context.Response().SetStatusCode(500)
// var e error
// if chain.Request.URL != nil {
// e = fmt.Errorf("ProxyChain error for '%s': %s", chain.Request.URL.String(), err.Error())
// } else {
// e = fmt.Errorf("ProxyChain error: '%s'", err.Error())
// }
// chain.Context.SendString(e.Error()) // <- RenderErrorPage middleware to render error
// log.Println(e.Error())
return err
}
// internal function to reset state of ProxyChain for reuse
func (chain *ProxyChain) _reset() {
chain.abortErr = nil
chain.Request = nil
// chain.Response = nil
chain.Context = nil
chain.onceResponseModifications = []ResponseModification{}
chain.onceRequestModifications = []RequestModification{}
// chain.onceClient = nil
}
// NewProxyChain initializes a new ProxyChain
func NewProxyChain() *ProxyChain {
chain := new(ProxyChain)
options := []tls_client.HttpClientOption{
tls_client.WithTimeoutSeconds(20),
//tls_client.WithRandomTLSExtensionOrder(),
tls_client.WithClientProfile(profiles.Chrome_117),
// tls_client.WithNotFollowRedirects(),
// tls_client.WithCookieJar(jar), // create cookieJar instance and pass it as argument
}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
if err != nil {
panic(err)
}
chain.Client = client
return chain
}
/// ========================================================================================================
// _execute sends the request for the ProxyChain and returns the raw body only
// the caller is responsible for returning a response back to the requestor
// the caller is also responsible for calling chain._reset() when they are done with the body
func (chain *ProxyChain) _execute() (io.Reader, error) {
// ================== PREFLIGHT CHECKS =============================
if chain.validateCtxIsSet() != nil || chain.abortErr != nil {
return nil, chain.abortErr
}
if chain.Request == nil {
return nil, errors.New("proxychain request not yet initialized")
}
if chain.Request.URL.Scheme == "" {
return nil, errors.New("request url not set or invalid. Check ProxyChain ReqMods for issues")
}
// ======== REQUEST MODIFICATIONS :: [client -> ladder] -> upstream -> ladder -> client =============================
// Apply requestModifications to proxychain
for _, applyRequestModificationsTo := range chain.requestModifications {
err := applyRequestModificationsTo(chain)
if err != nil {
return nil, chain.abort(err)
}
}
// Apply onceRequestModifications to proxychain and clear them
for len(chain.onceRequestModifications) > 0 {
i := 0
modFn := chain.onceRequestModifications[i]
err := modFn(chain)
if err != nil {
return nil, chain.abort(err)
}
// pop modFn off slice
chain.onceRequestModifications = append(chain.onceRequestModifications[:i], chain.onceRequestModifications[i+1:]...)
}
// ======== SEND REQUEST UPSTREAM :: client -> [ladder -> upstream] -> ladder -> client =============================
// Send Request Upstream
if chain.onceClient != nil {
// if chain.SetOnceClient() is used, use that client instead of the
// default http client temporarily.
resp, err := chain.onceClient.Do(chain.Request)
if err != nil {
return nil, chain.abort(err)
}
chain.Response = resp
// chain.onceClient = nil
} else {
resp, err := chain.Client.Do(chain.Request)
if err != nil {
return nil, chain.abort(err)
}
chain.Response = resp
}
// ======== APPLY RESPONSE MODIFIERS :: client -> ladder -> [upstream -> ladder] -> client =============================
// Apply ResponseModifiers to proxychain
for _, applyResultModificationsTo := range chain.responseModifications {
err := applyResultModificationsTo(chain)
if err != nil {
return nil, chain.abort(err)
}
}
// Apply onceResponseModifications to proxychain and clear them
for len(chain.onceResponseModifications) > 0 {
i := 0
modFn := chain.onceResponseModifications[i]
err := modFn(chain)
if err != nil {
return nil, chain.abort(err)
}
// pop modFn off slice
chain.onceResponseModifications = append(chain.onceResponseModifications[:i], chain.onceResponseModifications[i+1:]...)
}
// ======== RETURN BODY TO CLIENT :: client -> ladder -> upstream -> [ladder -> client] =============================
return chain.Response.Body, nil
}
// Execute sends the request for the ProxyChain and returns the request to the sender
// and resets the fields so that the ProxyChain can be reused.
// if any step in the ProxyChain fails, the request will abort and a 500 error will
// be returned to the client
func (chain *ProxyChain) Execute() error {
defer chain._reset()
body, err := chain._execute()
if err != nil {
log.Println(err)
return err
}
if chain.Context == nil {
return errors.New("no context set")
}
// TODO: this seems broken
// in case api user did not set or forward content-type, we do it for them
/*
ct := string(chain.Context.Response().Header.Peek("content-type"))
if ct == "" {
chain.Context.Set("content-type", chain.Response.Header.Get("content-type"))
}
*/
// Return request back to client
return chain.Context.SendStream(body)
// return chain.Context.SendStream(body)
}

View File

@@ -0,0 +1,11 @@
package proxychain
import (
"net/url"
)
type Pool map[url.URL]ProxyChain
func NewPool() Pool {
return map[url.URL]ProxyChain{}
}

View File

@@ -0,0 +1,29 @@
package requestmodifiers
import (
"math/rand"
"github.com/everywall/ladder/proxychain"
)
// AddCacheBusterQuery modifies query params to add a random parameter key
// In order to get the upstream network stack to serve a fresh copy of the page.
func AddCacheBusterQuery() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
ModifyQueryParams("ord", randomString(15)),
)
return nil
}
}
func randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789."
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}

View File

@@ -0,0 +1,142 @@
package bot
import (
"encoding/json"
"fmt"
"io"
"math/big"
"math/bits"
"math/rand"
"net"
"net/http"
"time"
)
type Bot interface {
UpdatePool() error
GetRandomIdentity() string
}
type bot struct {
UserAgent string
Fingerprint string
IPPool botPool
}
type botPool struct {
Timestamp string `json:"creationTime"`
Prefixes []botPrefix `json:"prefixes"`
}
type botPrefix struct {
IPv6 string `json:"ipv6Prefix,omitempty"`
IPv4 string `json:"ipv4Prefix,omitempty"`
}
// TODO: move pointers around, not global variables
var GoogleBot = bot{
UserAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; http://www.google.com/bot.html) Chrome/79.0.3945.120 Safari/537.36",
// https://github.com/trisulnsm/trisul-scripts/blob/master/lua/frontend_scripts/reassembly/ja3/prints/ja3fingerprint.json
Fingerprint: "769,49195-49199-49196-49200-52393-52392-52244-52243-49161-49171-49162-49172-156-157-47-53-10,65281-0-23-35-13-5-18-16-11-10-21,29-23-24,0",
IPPool: botPool{
Timestamp: "2023-11-28T23:00:56.000000",
Prefixes: []botPrefix{
{
IPv4: "34.100.182.96/28",
},
},
},
}
var BingBot = bot{
UserAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/79.0.3945.120 Safari/537.36",
IPPool: botPool{
Timestamp: "2023-03-08T10:00:00.121331",
Prefixes: []botPrefix{
{
IPv4: "207.46.13.0/24",
},
},
},
}
func (b *bot) UpdatePool(url string) error {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to update googlebot IP pool: status code %s", resp.Status)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, &b.IPPool)
return err
}
func (b *bot) GetRandomIP() string {
count := len(b.IPPool.Prefixes)
var prefix botPrefix
if count == 1 {
prefix = b.IPPool.Prefixes[0]
} else {
idx := rand.Intn(count)
prefix = b.IPPool.Prefixes[idx]
}
if prefix.IPv4 != "" {
ip, err := randomIPFromSubnet(prefix.IPv4)
if err == nil {
return ip.String()
}
}
if prefix.IPv6 != "" {
ip, err := randomIPFromSubnet(prefix.IPv6)
if err == nil {
return ip.String()
}
}
// fallback to default IP which is known to work
ip, _ := randomIPFromSubnet(b.IPPool.Prefixes[0].IPv4)
return ip.String()
}
func randomIPFromSubnet(c string) (net.IP, error) {
ip, ipnet, err := net.ParseCIDR(c)
if err != nil {
return nil, err
}
// int representation of byte mask
mask := big.NewInt(0).SetBytes(ipnet.Mask).Uint64()
// how many unset bits there are at the end of the mask
offset := bits.TrailingZeros8(byte(0) ^ byte(mask))
// total number of ips available in the block
offset *= offset
toAdd := rand.Intn(offset)
last := len(ip) - 1
ip[last] = ip[last] + byte(toAdd)
return ip, nil
}

View File

@@ -0,0 +1,36 @@
package bot
import (
"net"
"testing"
)
func TestRandomIPFromSubnet(t *testing.T) {
err := GoogleBot.UpdatePool("https://developers.google.com/static/search/apis/ipranges/googlebot.json")
if err != nil {
t.Error(err)
}
for _, prefix := range GoogleBot.IPPool.Prefixes {
subnet := prefix.IPv4
if prefix.IPv6 != "" {
subnet = prefix.IPv6
}
t.Run(subnet, func(t *testing.T) {
_, ipnet, err := net.ParseCIDR(subnet)
if err != nil {
t.Error(err)
}
ip, err := randomIPFromSubnet(subnet)
if err != nil {
t.Error(err)
}
if !ipnet.Contains(ip) {
t.Fail()
}
})
}
}

View File

@@ -0,0 +1,45 @@
package requestmodifiers
import (
"strings"
//"fmt"
"github.com/everywall/ladder/proxychain"
)
var forwardBlacklist map[string]bool
func init() {
forwardBlacklist = map[string]bool{
"host": true,
"connection": true,
"keep-alive": true,
"content-length": true,
"content-encoding": true,
"transfer-encoding": true,
"referer": true,
"x-forwarded-for": true,
"x-real-ip": true,
"forwarded": true,
"accept-encoding": true,
}
}
// ForwardRequestHeaders forwards the requests headers sent from the client to the upstream server
func ForwardRequestHeaders() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
forwardHeaders := func(key, value []byte) {
k := strings.ToLower(string(key))
v := string(value)
if forwardBlacklist[k] {
return
}
// fmt.Println(k, v)
chain.Request.Header.Set(k, v)
}
chain.Context.Request().
Header.VisitAll(forwardHeaders)
return nil
}
}

View File

@@ -0,0 +1,127 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain/requestmodifiers/bot"
"github.com/everywall/ladder/proxychain"
)
// MasqueradeAsGoogleBot modifies user agent and x-forwarded for
// to appear to be a Google Bot
func MasqueradeAsGoogleBot() proxychain.RequestModification {
ip := bot.GoogleBot.GetRandomIP()
return masqueradeAsTrustedBot(bot.GoogleBot.UserAgent, ip, bot.GoogleBot.Fingerprint)
}
// MasqueradeAsBingBot modifies user agent and x-forwarded for
// to appear to be a Bing Bot
func MasqueradeAsBingBot() proxychain.RequestModification {
ip := bot.BingBot.GetRandomIP()
return masqueradeAsTrustedBot(bot.BingBot.Fingerprint, ip, "")
}
// MasqueradeAsWaybackMachineBot modifies user agent and x-forwarded for
// to appear to be a archive.org (wayback machine) Bot
func MasqueradeAsWaybackMachineBot() proxychain.RequestModification {
const botUA string = "Mozilla/5.0 (compatible; archive.org_bot +http://www.archive.org/details/archive.org_bot)"
const botIP string = "207.241.235.164"
return masqueradeAsTrustedBot(botUA, botIP, "")
}
// MasqueradeAsFacebookBot modifies user agent and x-forwarded for
// to appear to be a Facebook Bot (link previews?)
func MasqueradeAsFacebookBot() proxychain.RequestModification {
const botUA string = "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)"
// 31.13.97.0/24, 31.13.99.0/24, 31.13.100.0/24, 66.220.144.0/20, 69.63.189.0/24, 69.63.190.0/24, 69.171.224.0/20, 69.171.240.0/21, 69.171.248.0/24, 173.252.73.0/24, 173.252.74.0/24, 173.252.77.0/24, 173.252.100.0/22, 173.252.104.0/21, 173.252.112.0/24, 2a03:2880:10::/48, 2a03:2880:10ff::/48, 2a03:2880:11::/48, 2a03:2880:11ff::/48, 2a03:2880:20::/48, 2a03:2880:20ff::/48, 2a03:2880:21ff::/48, 2a03:2880:30ff::/48, 2a03:2880:31ff::/48, 2a03:2880:1010::/48, 2a03:2880:1020::/48, 2a03:2880:2020::/48, 2a03:2880:2050::/48, 2a03:2880:2040::/48, 2a03:2880:2110::/48, 2a03:2880:2130::/48, 2a03:2880:3010::/48, 2a03:2880:3020::/48
const botIP string = "31.13.99.8"
const ja3 string = "771,49199-49195-49171-49161-49200-49196-49172-49162-51-57-50-49169-49159-47-53-10-5-4-255,0-11-10-13-13172-16,23-25-28-27-24-26-22-14-13-11-12-9-10,0-1-2"
return masqueradeAsTrustedBot(botUA, botIP, ja3)
}
// MasqueradeAsYandexBot modifies user agent and x-forwarded for
// to appear to be a Yandex Spider Bot
func MasqueradeAsYandexBot() proxychain.RequestModification {
const botUA string = "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)"
// 100.43.90.0/24, 37.9.115.0/24, 37.140.165.0/24, 77.88.22.0/25, 77.88.29.0/24, 77.88.31.0/24, 77.88.59.0/24, 84.201.146.0/24, 84.201.148.0/24, 84.201.149.0/24, 87.250.243.0/24, 87.250.253.0/24, 93.158.147.0/24, 93.158.148.0/24, 93.158.151.0/24, 93.158.153.0/32, 95.108.128.0/24, 95.108.138.0/24, 95.108.150.0/23, 95.108.158.0/24, 95.108.156.0/24, 95.108.188.128/25, 95.108.234.0/24, 95.108.248.0/24, 100.43.80.0/24, 130.193.62.0/24, 141.8.153.0/24, 178.154.165.0/24, 178.154.166.128/25, 178.154.173.29, 178.154.200.158, 178.154.202.0/24, 178.154.205.0/24, 178.154.239.0/24, 178.154.243.0/24, 37.9.84.253, 199.21.99.99, 178.154.162.29, 178.154.203.251, 178.154.211.250, 178.154.171.0/24, 178.154.200.0/24, 178.154.244.0/24, 178.154.246.0/24, 95.108.181.0/24, 95.108.246.252, 5.45.254.0/24, 5.255.253.0/24, 37.140.141.0/24, 37.140.188.0/24, 100.43.81.0/24, 100.43.85.0/24, 100.43.91.0/24, 199.21.99.0/24, 2a02:6b8:b000::/32, 2a02:6b8:b010::/32, 2a02:6b8:b011::/32, 2a02:6b8:c0e::/32
const botIP string = "37.9.115.9"
const ja3 string = "769,49200-49196-49192-49188-49172-49162-165-163-161-159-107-106-105-104-57-56-55-54-136-135-134-133-49202-49198-49194-49190-49167-49157-157-61-53-132-49199-49195-49191-49187-49171-49161-164-162-160-158-103-64-63-62-51-50-49-48-154-153-152-151-69-68-67-66-49201-49197-49193-49189-49166-49156-156-60-47-150-65-7-49169-49159-49164-49154-5-4-49170-49160-22-19-16-13-49165-49155-10-255,0-11-10-35-13-15,23-25-28-27-24-26-22-14-13-11-12-9-10,0-1-2"
return masqueradeAsTrustedBot(botUA, botIP, ja3)
}
// MasqueradeAsBaiduBot modifies user agent and x-forwarded for
// to appear to be a Baidu Spider Bot
func MasqueradeAsBaiduBot() proxychain.RequestModification {
const botUA string = "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
// 180.76.15.0/24, 119.63.196.0/24, 115.239.212./24, 119.63.199.0/24, 122.81.208.0/22, 123.125.71.0/24, 180.76.4.0/24, 180.76.5.0/24, 180.76.6.0/24, 185.10.104.0/24, 220.181.108.0/24, 220.181.51.0/24, 111.13.102.0/24, 123.125.67.144/29, 123.125.67.152/31, 61.135.169.0/24, 123.125.68.68/30, 123.125.68.72/29, 123.125.68.80/28, 123.125.68.96/30, 202.46.48.0/20, 220.181.38.0/24, 123.125.68.80/30, 123.125.68.84/31, 123.125.68.0/24
const botIP string = "180.76.15.7"
return masqueradeAsTrustedBot(botUA, botIP, "")
}
// MasqueradeAsDuckDuckBot modifies user agent and x-forwarded for
// to appear to be a DuckDuckGo Bot
func MasqueradeAsDuckDuckBot() proxychain.RequestModification {
const botUA string = "DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)"
// 46.51.197.88, 46.51.197.89, 50.18.192.250, 50.18.192.251, 107.21.1.61, 176.34.131.233, 176.34.135.167, 184.72.106.52, 184.72.115.86
const botIP string = "46.51.197.88"
return masqueradeAsTrustedBot(botUA, botIP, "")
}
// MasqueradeAsYahooBot modifies user agent and x-forwarded for
// to appear to be a Yahoo Bot
func MasqueradeAsYahooBot() proxychain.RequestModification {
const botUA string = "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)"
// 5.255.250.0/24, 37.9.87.0/24, 67.195.37.0/24, 67.195.50.0/24, 67.195.110.0/24, 67.195.111.0/24, 67.195.112.0/23, 67.195.114.0/24, 67.195.115.0/24, 68.180.224.0/21, 72.30.132.0/24, 72.30.142.0/24, 72.30.161.0/24, 72.30.196.0/24, 72.30.198.0/24, 74.6.254.0/24, 74.6.8.0/24, 74.6.13.0/24, 74.6.17.0/24, 74.6.18.0/24, 74.6.22.0/24, 74.6.27.0/24, 74.6.168.0/24, 77.88.5.0/24, 77.88.47.0/24, 93.158.161.0/24, 98.137.72.0/24, 98.137.206.0/24, 98.137.207.0/24, 98.139.168.0/24, 114.111.95.0/24, 124.83.159.0/24, 124.83.179.0/24, 124.83.223.0/24, 141.8.144.0/24, 183.79.63.0/24, 183.79.92.0/24, 203.216.255.0/24, 211.14.11.0/24
const ja3 = "769,49200-49196-49192-49188-49172-49162-163-159-107-106-57-56-136-135-49202-49198-49194-49190-49167-49157-157-61-53-132-49199-49195-49191-49187-49171-49161-162-158-103-64-51-50-49170-49160-154-153-69-68-22-19-49201-49197-49193-49189-49166-49156-49165-49155-156-60-47-150-65-10-7-49169-49159-49164-49154-5-4-255,0-11-10-13-15,25-24-23,0-1-2"
const botIP string = "37.9.87.5"
return masqueradeAsTrustedBot(botUA, botIP, ja3)
}
func masqueradeAsTrustedBot(botUA string, botIP string, ja3 string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofUserAgent(botUA),
// general / nginx
SetRequestHeader("X-Forwarded-For", botIP),
SetRequestHeader("X-Real-IP", botIP),
SetRequestHeader("True-Client-IP", botIP),
SetRequestHeader("WL-Proxy-Client-IP", botIP),
SetRequestHeader("X-Cluster-Client-IP", botIP),
/*
// akamai
SetRequestHeader("True-Client-IP", botIP),
// cloudflare
// TODO: this seems to cause issues with CF... figure out workaround or remove
Error 1000
Ray ID: xxxxxxxxxxxxxxxx •
2023-12-01 20:09:22 UTC
DNS points to prohibited IP
What happened?
You've requested a page on a website (xxxxxxxxxxxxxxxxxxx) that is on the Cloudflare network. Unfortunately, it is resolving to an IP address that is creating a conflict within Cloudflare's system
SetRequestHeader("CF-Connecting-IP", botIP),
// weblogic
SetRequestHeader("WL-Proxy-Client-IP", botIP),
// azure
SetRequestHeader("X-Cluster-Client-IP", botIP),
*/
DeleteRequestHeader("referrer"),
DeleteRequestHeader("origin"),
)
/*
if ja3 != "" {
chain.AddOnceRequestModifications(
SpoofJA3fingerprint(ja3, botUA),
)
}
*/
return nil
}
}

View File

@@ -0,0 +1,19 @@
package requestmodifiers
import (
"fmt"
"regexp"
"github.com/everywall/ladder/proxychain"
)
func ModifyDomainWithRegex(matchRegex string, replacement string) proxychain.RequestModification {
match, err := regexp.Compile(matchRegex)
return func(px *proxychain.ProxyChain) error {
if err != nil {
return fmt.Errorf("RequestModification :: ModifyDomainWithRegex error => invalid match regex: %s - %s", matchRegex, err.Error())
}
px.Request.URL.Host = match.ReplaceAllString(px.Request.URL.Host, replacement)
return nil
}
}

View File

@@ -0,0 +1,100 @@
package requestmodifiers
import (
//"net/http"
//http "github.com/Danny-Dasilva/fhttp"
http "github.com/bogdanfinn/fhttp"
"github.com/everywall/ladder/proxychain"
)
// SetOutgoingCookie modifes a specific cookie name
// by modifying the request cookie headers going to the upstream server.
// If the cookie name does not already exist, it is created.
func SetOutgoingCookie(name string, val string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
cookies := chain.Request.Cookies()
hasCookie := false
for _, cookie := range cookies {
if cookie.Name != name {
continue
}
hasCookie = true
cookie.Value = val
}
if hasCookie {
return nil
}
chain.Request.AddCookie(&http.Cookie{
Domain: chain.Request.URL.Host,
Name: name,
Value: val,
})
return nil
}
}
// SetOutgoingCookies modifies a client request's cookie header
// to a raw Cookie string, overwriting existing cookies
func SetOutgoingCookies(cookies string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.Request.Header.Set("Cookies", cookies)
return nil
}
}
// DeleteOutgoingCookie modifies the http request's cookies header to
// delete a specific request cookie going to the upstream server.
// If the cookie does not exist, it does not do anything.
func DeleteOutgoingCookie(name string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
cookies := chain.Request.Cookies()
chain.Request.Header.Del("Cookies")
for _, cookie := range cookies {
if cookie.Name == name {
chain.Request.AddCookie(cookie)
}
}
return nil
}
}
// DeleteOutgoingCookies removes the cookie header entirely,
// preventing any cookies from reaching the upstream server.
func DeleteOutgoingCookies() proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Del("Cookie")
return nil
}
}
// DeleteOutGoingCookiesExcept prevents non-whitelisted cookies from being sent from the client
// to the upstream proxy server. Cookies whose names are in the whitelist are not removed.
func DeleteOutgoingCookiesExcept(whitelist ...string) proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
// Convert whitelist slice to a map for efficient lookups
whitelistMap := make(map[string]struct{})
for _, cookieName := range whitelist {
whitelistMap[cookieName] = struct{}{}
}
// Get all cookies from the request header
cookies := px.Request.Cookies()
// Clear the original Cookie header
px.Request.Header.Del("Cookie")
// Re-add cookies that are in the whitelist
for _, cookie := range cookies {
if _, found := whitelistMap[cookie.Name]; found {
px.Request.AddCookie(cookie)
}
}
return nil
}
}

View File

@@ -0,0 +1,19 @@
package requestmodifiers
import (
"fmt"
"regexp"
"github.com/everywall/ladder/proxychain"
)
func ModifyPathWithRegex(matchRegex string, replacement string) proxychain.RequestModification {
match, err := regexp.Compile(matchRegex)
return func(px *proxychain.ProxyChain) error {
if err != nil {
return fmt.Errorf("RequestModification :: ModifyPathWithRegex error => invalid match regex: %s - %s", matchRegex, err.Error())
}
px.Request.URL.Path = match.ReplaceAllString(px.Request.URL.Path, replacement)
return nil
}
}

View File

@@ -0,0 +1,28 @@
package requestmodifiers
import (
//"fmt"
"net/url"
"github.com/everywall/ladder/proxychain"
)
// ModifyQueryParams replaces query parameter values in URL's query params in a ProxyChain's URL.
// If the query param key doesn't exist, it is created.
func ModifyQueryParams(key string, value string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
q := chain.Request.URL.Query()
chain.Request.URL.RawQuery = modifyQueryParams(key, value, q)
//fmt.Println(chain.Request.URL.String())
return nil
}
}
func modifyQueryParams(key string, value string, q url.Values) string {
if value == "" {
q.Del(key)
return q.Encode()
}
q.Set(key, value)
return q.Encode()
}

View File

@@ -0,0 +1,23 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SetRequestHeader modifies a specific outgoing header
// This is the header that the upstream server will see.
func SetRequestHeader(name string, val string) proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Set(name, val)
return nil
}
}
// DeleteRequestHeader modifies a specific outgoing header
// This is the header that the upstream server will see.
func DeleteRequestHeader(name string) proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Del(name)
return nil
}
}

View File

@@ -0,0 +1,48 @@
package requestmodifiers
import (
"fmt"
"net/url"
"regexp"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
)
const archivistUrl string = "https://archive.is/latest"
// RequestArchiveIs modifies a ProxyChain's URL to request an archived version from archive.is
func RequestArchiveIs() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
rURL := preventRecursiveArchivistURLs(chain.Request.URL.String())
chain.Request.URL.RawQuery = ""
newURL, err := url.Parse(fmt.Sprintf("%s/%s", archivistUrl, rURL))
if err != nil {
return err
}
// archivist seems to sabotage requests from cloudflare's DNS
// bypass this just in case
chain.AddOnceRequestModifications(ResolveWithGoogleDoH())
chain.Request.URL = newURL
// cleanup archivst headers
script := `[...document.querySelector("body > center").childNodes].filter(e => e.id != "SOLID").forEach(e => e.remove())`
chain.AddOnceResponseModifications(
tx.InjectScriptAfterDOMContentLoaded(script),
)
return nil
}
}
// https://archive.is/20200421201055/https://rt.live/ -> http://rt.live/
func preventRecursiveArchivistURLs(url string) string {
re := regexp.MustCompile(`https?:\/\/archive\.is\/\d+\/(https?:\/\/.*)`)
match := re.FindStringSubmatch(url)
if match != nil {
return match[1]
}
return url
}

View File

@@ -0,0 +1,22 @@
package requestmodifiers
import (
"net/url"
"github.com/everywall/ladder/proxychain"
)
const googleCacheUrl string = "https://webcache.googleusercontent.com/search?q=cache:"
// RequestGoogleCache modifies a ProxyChain's URL to request its Google Cache version.
func RequestGoogleCache() proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
encodedURL := url.QueryEscape(px.Request.URL.String())
newURL, err := url.Parse(googleCacheUrl + encodedURL)
if err != nil {
return err
}
px.Request.URL = newURL
return nil
}
}

View File

@@ -0,0 +1,44 @@
package requestmodifiers
import (
"net/url"
"regexp"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
)
const waybackUrl string = "https://web.archive.org/web/"
// RequestWaybackMachine modifies a ProxyChain's URL to request the wayback machine (archive.org) version.
func RequestWaybackMachine() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.Request.URL.RawQuery = ""
rURL := preventRecursiveWaybackURLs(chain.Request.URL.String())
newURLString := waybackUrl + rURL
newURL, err := url.Parse(newURLString)
if err != nil {
return err
}
chain.Request.URL = newURL
// cleanup wayback headers
script := `["wm-ipp-print", "wm-ipp-base"].forEach(id => { try { document.getElementById(id).remove() } catch{ } })`
chain.AddOnceResponseModifications(
tx.InjectScriptAfterDOMContentLoaded(script),
)
return nil
}
}
func preventRecursiveWaybackURLs(url string) string {
re := regexp.MustCompile(`https:\/\/web\.archive\.org\/web\/\d+\/\*(https?:\/\/.*)`)
match := re.FindStringSubmatch(url)
if match != nil {
return match[1]
}
return url
}

View File

@@ -0,0 +1,94 @@
package requestmodifiers
import (
"context"
"encoding/json"
"fmt"
"net"
"time"
http "github.com/bogdanfinn/fhttp"
/*
tls_client "github.com/bogdanfinn/tls-client"
//"net/http"
*/
"github.com/everywall/ladder/proxychain"
)
// resolveWithGoogleDoH resolves DNS using Google's DNS-over-HTTPS
func resolveWithGoogleDoH(host string) (string, error) {
url := "https://dns.google/resolve?name=" + host + "&type=A"
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Answer []struct {
Data string `json:"data"`
} `json:"Answer"`
}
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return "", err
}
// Get the first A record
if len(result.Answer) > 0 {
return result.Answer[0].Data, nil
}
return "", fmt.Errorf("no DoH DNS record found for %s", host)
}
type CustomDialer struct {
*net.Dialer
}
func newCustomDialer(timeout, keepAlive time.Duration) *CustomDialer {
return &CustomDialer{
Dialer: &net.Dialer{
Timeout: timeout,
KeepAlive: keepAlive,
},
}
}
func (cd *CustomDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
port = "443"
}
resolvedHost, err := resolveWithGoogleDoH(host)
if err != nil {
return nil, err
}
return cd.Dialer.DialContext(ctx, network, net.JoinHostPort(resolvedHost, port))
}
// ResolveWithGoogleDoH modifies a ProxyChain's client to make the request by resolving the URL
// using Google's DNS over HTTPs service
func ResolveWithGoogleDoH() proxychain.RequestModification {
///customDialer := NewCustomDialer(10*time.Second, 10*time.Second)
return func(chain *proxychain.ProxyChain) error {
/*
options := []tls_client.HttpClientOption{
tls_client.WithTimeoutSeconds(30),
tls_client.WithRandomTLSExtensionOrder(),
tls_client.WithDialer(*customDialer.Dialer),
//tls_client.WithClientProfile(profiles.Chrome_105),
}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
if err != nil {
return err
}
chain.SetOnceHTTPClient(client)
*/
return nil
}
}

View File

@@ -0,0 +1,24 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofOrigin modifies the origin header
// if the upstream server returns a Vary header
// it means you might get a different response if you change this
func SpoofOrigin(url string) proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Set("origin", url)
return nil
}
}
// HideOrigin modifies the origin header
// so that it is the original origin, not the proxy
func HideOrigin() proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Set("origin", px.Request.URL.String())
return nil
}
}

View File

@@ -0,0 +1,39 @@
package requestmodifiers
import (
"fmt"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrer modifies the referrer header.
// It is useful if the page can be accessed from a search engine
// or social media site, but not by browsing the website itself.
// if url is "", then the referrer header is removed.
func SpoofReferrer(url string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
// change refer on client side js
script := fmt.Sprintf(`document.referrer = "%s"`, url)
chain.AddOnceResponseModifications(
tx.InjectScriptBeforeDOMContentLoaded(script),
)
if url == "" {
chain.Request.Header.Del("referrer")
return nil
}
chain.Request.Header.Set("referrer", url)
return nil
}
}
// HideReferrer modifies the referrer header
// so that it is the original referrer, not the proxy
func HideReferrer() proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Set("referrer", px.Request.URL.String())
return nil
}
}

View File

@@ -0,0 +1,43 @@
package requestmodifiers
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromBaiduSearch modifies the referrer header
// pretending to be from a BaiduSearch
func SpoofReferrerFromBaiduSearch() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
// https://www.baidu.com/link?url=5biIeDvUIihawf3Zbbysach2Xn4H3w3FzO6LZKgSs-B5Yt4M4RUFikokOk5zetf2&wd=&eqid=9da80d8208009b8480000706655d5ed6
referrer := fmt.Sprintf("https://baidu.com/link?url=%s", generateRandomBaiduURL())
chain.Request.Header.Set("referrer", referrer)
chain.Request.Header.Set("sec-fetch-site", "cross-site")
chain.Request.Header.Set("sec-fetch-dest", "document")
chain.Request.Header.Set("sec-fetch-mode", "navigate")
return nil
}
}
// utility functions ==================
func generateRandomString(charset string, length int) string {
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
var stringBuilder strings.Builder
for i := 0; i < length; i++ {
stringBuilder.WriteByte(charset[seededRand.Intn(len(charset))])
}
return stringBuilder.String()
}
func generateRandomBaiduURL() string {
const alphanumericCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const hexCharset = "0123456789abcdef"
randomAlphanumeric := generateRandomString(alphanumericCharset, 30) // Length before "-"
randomHex := generateRandomString(hexCharset, 16) // Length of eqid
return randomAlphanumeric + "-" + "&wd=&eqid=" + randomHex
}

View File

@@ -0,0 +1,20 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromBingSearch modifies the referrer header
// pretending to be from a bing search site
func SpoofReferrerFromBingSearch() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofReferrer("https://www.bing.com/"),
SetRequestHeader("sec-fetch-site", "cross-site"),
SetRequestHeader("sec-fetch-dest", "document"),
SetRequestHeader("sec-fetch-mode", "navigate"),
ModifyQueryParams("utm_source", "bing"),
)
return nil
}
}

View File

@@ -0,0 +1,20 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromGoogleSearch modifies the referrer header
// pretending to be from a google search site
func SpoofReferrerFromGoogleSearch() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofReferrer("https://www.google.com"),
SetRequestHeader("sec-fetch-site", "cross-site"),
SetRequestHeader("sec-fetch-dest", "document"),
SetRequestHeader("sec-fetch-mode", "navigate"),
ModifyQueryParams("utm_source", "google"),
)
return nil
}
}

View File

@@ -0,0 +1,21 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromLinkedInPost modifies the referrer header
// pretending to be from a linkedin post
func SpoofReferrerFromLinkedInPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofReferrer("https://www.linkedin.com/"),
SetRequestHeader("sec-fetch-site", "cross-site"),
SetRequestHeader("sec-fetch-dest", "document"),
SetRequestHeader("sec-fetch-mode", "navigate"),
ModifyQueryParams("utm_campaign", "post"),
ModifyQueryParams("utm_medium", "web"),
)
return nil
}
}

View File

@@ -0,0 +1,23 @@
package requestmodifiers
import (
"fmt"
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromNaverSearch modifies the referrer header
// pretending to be from a Naver search (popular in South Korea)
func SpoofReferrerFromNaverSearch() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
referrer := fmt.Sprintf(
"https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=%s",
chain.Request.URL.Host,
)
chain.Request.Header.Set("referrer", referrer)
chain.Request.Header.Set("sec-fetch-site", "cross-site")
chain.Request.Header.Set("sec-fetch-dest", "document")
chain.Request.Header.Set("sec-fetch-mode", "navigate")
return nil
}
}

View File

@@ -0,0 +1,17 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromPinterestPost modifies the referrer header
// pretending to be from a pinterest post
func SpoofReferrerFromPinterestPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.Request.Header.Set("referrer", "https://www.pinterest.com/")
chain.Request.Header.Set("sec-fetch-site", "cross-site")
chain.Request.Header.Set("sec-fetch-dest", "document")
chain.Request.Header.Set("sec-fetch-mode", "navigate")
return nil
}
}

View File

@@ -0,0 +1,16 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromQQPost modifies the referrer header
// pretending to be from a QQ post (popular social media in China)
func SpoofReferrerFromQQPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.Request.Header.Set("referrer", "https://new.qq.com/")
chain.Request.Header.Set("sec-fetch-site", "cross-site")
chain.Request.Header.Set("sec-fetch-dest", "document")
return nil
}
}

View File

@@ -0,0 +1,17 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromRedditPost modifies the referrer header
// pretending to be from a reddit post
func SpoofReferrerFromRedditPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.Request.Header.Set("referrer", "https://www.reddit.com/")
chain.Request.Header.Set("sec-fetch-site", "cross-site")
chain.Request.Header.Set("sec-fetch-dest", "document")
chain.Request.Header.Set("sec-fetch-mode", "navigate")
return nil
}
}

View File

@@ -0,0 +1,19 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromTumblrPost modifies the referrer header
// pretending to be from a tumblr post
func SpoofReferrerFromTumblrPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofReferrer("https://www.tumblr.com/"),
SetRequestHeader("sec-fetch-site", "cross-site"),
SetRequestHeader("sec-fetch-dest", "document"),
SetRequestHeader("sec-fetch-mode", "navigate"),
)
return nil
}
}

View File

@@ -0,0 +1,19 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromTwitterPost modifies the referrer header
// pretending to be from a twitter post
func SpoofReferrerFromTwitterPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofReferrer("https://t.co/"),
SetRequestHeader("sec-fetch-site", "cross-site"),
SetRequestHeader("sec-fetch-dest", "document"),
SetRequestHeader("sec-fetch-mode", "navigate"),
)
return nil
}
}

View File

@@ -0,0 +1,19 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromVkontaktePost modifies the referrer header
// pretending to be from a vkontakte post (popular in Russia)
func SpoofReferrerFromVkontaktePost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceRequestModifications(
SpoofReferrer("https://away.vk.com/"),
SetRequestHeader("sec-fetch-site", "cross-site"),
SetRequestHeader("sec-fetch-dest", "document"),
SetRequestHeader("sec-fetch-mode", "navigate"),
)
return nil
}
}

View File

@@ -0,0 +1,21 @@
package requestmodifiers
import (
"fmt"
"math/rand"
"github.com/everywall/ladder/proxychain"
)
// SpoofReferrerFromWeiboPost modifies the referrer header
// pretending to be from a Weibo post (popular in China)
func SpoofReferrerFromWeiboPost() proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
referrer := fmt.Sprintf("http://weibo.com/u/%d", rand.Intn(90001))
chain.Request.Header.Set("referrer", referrer)
chain.Request.Header.Set("sec-fetch-site", "cross-site")
chain.Request.Header.Set("sec-fetch-dest", "document")
chain.Request.Header.Set("sec-fetch-mode", "navigate")
return nil
}
}

View File

@@ -0,0 +1,41 @@
package requestmodifiers
import (
_ "embed"
"strings"
tx "github.com/everywall/ladder/proxychain/responsemodifiers"
"github.com/everywall/ladder/proxychain"
)
// https://github.com/faisalman/ua-parser-js/tree/master
// update using:
// git submodule update --remote --merge
//
//go:embed vendor/ua-parser-js/dist/ua-parser.min.js
var UAParserJS string
// note: spoof_user_agent.js has a dependency on ua-parser.min.js
// ua-parser.min.js should be loaded first.
//
//go:embed spoof_user_agent.js
var spoofUserAgentJS string
// SpoofUserAgent modifies the user agent
func SpoofUserAgent(ua string) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
// modify ua headers
chain.AddOnceRequestModifications(
SetRequestHeader("user-agent", ua),
)
script := strings.ReplaceAll(spoofUserAgentJS, "{{USER_AGENT}}", ua)
chain.AddOnceResponseModifications(
tx.InjectScriptBeforeDOMContentLoaded(script),
tx.InjectScriptBeforeDOMContentLoaded(UAParserJS),
)
return nil
}
}

View File

@@ -0,0 +1,100 @@
(() => {
const UA = "{{USER_AGENT}}";
// monkey-patch navigator.userAgent
{
const { get } = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"userAgent",
);
Object.defineProperty(Navigator.prototype, "userAgent", {
get: new Proxy(get, {
apply() {
return UA;
},
}),
});
}
// monkey-patch navigator.appVersion
{
const { get } = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"appVersion",
);
Object.defineProperty(Navigator.prototype, "appVersion", {
get: new Proxy(get, {
apply() {
return UA.replace("Mozilla/", "");
},
}),
});
}
// monkey-patch navigator.UserAgentData
// Assuming UAParser is already loaded and available
function spoofUserAgentData(uaString) {
// Parse the user-agent string
const parser = new UAParser(uaString);
const parsedData = parser.getResult();
// Extracted data
const platform = parsedData.os.name;
const browserName = parsedData.browser.name;
const browserMajorVersion = parsedData.browser.major;
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
uaString,
);
// Overwrite navigator.userAgentData
self.NavigatorUAData = self.NavigatorUAData || new class NavigatorUAData {
brands = [{
brand: browserName,
version: browserMajorVersion,
}];
mobile = isMobile;
platform = platform;
toJSON() {
return {
brands: this.brands,
mobile: this.mobile,
platform: this.platform,
};
}
getHighEntropyValues(hints) {
const result = this.toJSON();
// Add additional high entropy values based on hints
// Modify these as per your requirements
if (hints.includes("architecture")) {
result.architecture = "x86";
}
if (hints.includes("bitness")) {
result.bitness = "64";
}
if (hints.includes("model")) {
result.model = "";
}
if (hints.includes("platformVersion")) {
result.platformVersion = "10.0.0"; // Example value
}
if (hints.includes("uaFullVersion")) {
result.uaFullVersion = browserMajorVersion;
}
if (hints.includes("fullVersionList")) {
result.fullVersionList = this.brands;
}
return Promise.resolve(result);
}
}();
// Apply the monkey patch
Object.defineProperty(navigator, "userAgentData", {
value: new self.NavigatorUAData(),
writable: false,
});
}
spoofUserAgentData(UA);
// TODO: use hideMonkeyPatch to hide overrides
})();

View File

@@ -0,0 +1,14 @@
package requestmodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SpoofXForwardedFor modifies the X-Forwarded-For header
// in some cases, a forward proxy may interpret this as the source IP
func SpoofXForwardedFor(ip string) proxychain.RequestModification {
return func(px *proxychain.ProxyChain) error {
px.Request.Header.Set("X-FORWARDED-FOR", ip)
return nil
}
}

View File

@@ -0,0 +1,56 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"io"
"reflect"
)
type Error struct {
Success bool `json:"success"`
Error ErrorDetails `json:"error"`
}
type ErrorDetails struct {
Message string `json:"message"`
Type string `json:"type"`
Cause string `json:"cause"`
}
func CreateAPIErrReader(err error) io.ReadCloser {
if err == nil {
return io.NopCloser(bytes.NewBufferString(`{"success":false, "error": "No error provided"}`))
}
baseErr := getBaseError(err)
apiErr := Error{
Success: false,
Error: ErrorDetails{
Message: err.Error(),
Type: reflect.TypeOf(err).String(),
Cause: baseErr.Error(),
},
}
// Serialize the APIError into JSON
jsonData, jsonErr := json.Marshal(apiErr)
if jsonErr != nil {
return io.NopCloser(bytes.NewBufferString(`{"success":false, "error": "Failed to serialize error"}`))
}
// Return the JSON data as an io.ReadCloser
return io.NopCloser(bytes.NewBuffer(jsonData))
}
func getBaseError(err error) error {
for {
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
return err
}
err = unwrapped
}
}

View File

@@ -0,0 +1,174 @@
package api
import (
"github.com/go-shiori/dom"
"github.com/markusmobius/go-trafilatura"
"golang.org/x/net/html"
)
// =======================================================================================
// credit @joncrangle https://github.com/everywall/ladder/issues/38#issuecomment-1831252934
type ImageContent struct {
Type string `json:"type"`
URL string `json:"url"`
Alt string `json:"alt"`
Caption string `json:"caption"`
}
type LinkContent struct {
Type string `json:"type"`
Href string `json:"href"`
Data string `json:"data"`
}
type TextContent struct {
Type string `json:"type"`
Data string `json:"data"`
}
type ListContent struct {
Type string `json:"type"`
ListItems []ListItemContent `json:"listItems"`
}
type ListItemContent struct {
Data string `json:"data"`
}
type JSONDocument struct {
Success bool `json:"success"`
Error ErrorDetails `json:"error"`
Metadata struct {
Title string `json:"title"`
Author string `json:"author"`
URL string `json:"url"`
Hostname string `json:"hostname"`
Image string `json:"image"`
Description string `json:"description"`
Sitename string `json:"sitename"`
Date string `json:"date"`
Categories []string `json:"categories"`
Tags []string `json:"tags"`
License string `json:"license"`
} `json:"metadata"`
Content []interface{} `json:"content"`
Comments string `json:"comments"`
}
func ExtractResultToAPIResponse(extract *trafilatura.ExtractResult) *JSONDocument {
jsonDoc := &JSONDocument{}
// Populate success
jsonDoc.Success = true
// Populate metadata
jsonDoc.Metadata.Title = extract.Metadata.Title
jsonDoc.Metadata.Author = extract.Metadata.Author
jsonDoc.Metadata.URL = extract.Metadata.URL
jsonDoc.Metadata.Hostname = extract.Metadata.Hostname
jsonDoc.Metadata.Description = extract.Metadata.Description
jsonDoc.Metadata.Image = extract.Metadata.Image
jsonDoc.Metadata.Sitename = extract.Metadata.Sitename
jsonDoc.Metadata.Date = extract.Metadata.Date.Format("2006-01-02")
jsonDoc.Metadata.Categories = extract.Metadata.Categories
jsonDoc.Metadata.Tags = extract.Metadata.Tags
jsonDoc.Metadata.License = extract.Metadata.License
jsonDoc.Metadata.Hostname = extract.Metadata.Hostname
// Populate content
if extract.ContentNode != nil {
jsonDoc.Content = parseContent(extract.ContentNode)
}
// Populate comments
if extract.CommentsNode != nil {
jsonDoc.Comments = dom.OuterHTML(extract.CommentsNode)
}
return jsonDoc
}
func parseContent(node *html.Node) []interface{} {
var content []interface{}
for child := node.FirstChild; child != nil; child = child.NextSibling {
switch child.Data {
case "img":
image := ImageContent{
Type: "img",
URL: dom.GetAttribute(child, "src"),
Alt: dom.GetAttribute(child, "alt"),
Caption: dom.GetAttribute(child, "caption"),
}
content = append(content, image)
case "a":
link := LinkContent{
Type: "a",
Href: dom.GetAttribute(child, "href"),
Data: dom.InnerText(child),
}
content = append(content, link)
case "h1":
text := TextContent{
Type: "h1",
Data: dom.InnerText(child),
}
content = append(content, text)
case "h2":
text := TextContent{
Type: "h2",
Data: dom.InnerText(child),
}
content = append(content, text)
case "h3":
text := TextContent{
Type: "h3",
Data: dom.InnerText(child),
}
content = append(content, text)
case "h4":
text := TextContent{
Type: "h4",
Data: dom.InnerText(child),
}
content = append(content, text)
case "h5":
text := TextContent{
Type: "h5",
Data: dom.InnerText(child),
}
content = append(content, text)
case "ul", "ol":
list := ListContent{
Type: child.Data,
ListItems: []ListItemContent{},
}
for listItem := child.FirstChild; listItem != nil; listItem = listItem.NextSibling {
if listItem.Data == "li" {
listItemContent := ListItemContent{
Data: dom.InnerText(listItem),
}
list.ListItems = append(list.ListItems, listItemContent)
}
}
content = append(content, list)
default:
text := TextContent{
Type: "p",
Data: dom.InnerText(child),
}
content = append(content, text)
}
}
return content
}

View File

@@ -0,0 +1,48 @@
package responsemodifiers
import (
"bytes"
"encoding/json"
"io"
"github.com/markusmobius/go-trafilatura"
"github.com/everywall/ladder/proxychain/responsemodifiers/api"
"github.com/everywall/ladder/proxychain"
)
// APIContent creates an JSON representation of the article and returns it as an API response.
func APIContent() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// we set content-type twice here, in case another response modifier
// tries to forward over the original headers
chain.Context.Set("content-type", "application/json")
chain.Response.Header.Set("content-type", "application/json")
// extract dom contents
opts := trafilatura.Options{
IncludeImages: true,
IncludeLinks: true,
// FavorPrecision: true,
FallbackCandidates: nil, // TODO: https://github.com/markusmobius/go-trafilatura/blob/main/examples/chained/main.go
// implement fallbacks from "github.com/markusmobius/go-domdistiller" and "github.com/go-shiori/go-readability"
OriginalURL: chain.Request.URL,
}
result, err := trafilatura.Extract(chain.Response.Body, opts)
if err != nil {
chain.Response.Body = api.CreateAPIErrReader(err)
return nil
}
res := api.ExtractResultToAPIResponse(result)
jsonData, err := json.MarshalIndent(res, "", " ")
if err != nil {
return err
}
chain.Response.Body = io.NopCloser(bytes.NewReader(jsonData))
return nil
}
}

View File

@@ -0,0 +1,70 @@
package responsemodifiers
import (
"encoding/json"
"fmt"
"io"
"net/url"
"testing"
"github.com/everywall/ladder/proxychain/responsemodifiers/api"
)
func TestCreateAPIErrReader(t *testing.T) {
_, baseErr := url.Parse("://this is an invalid url")
wrappedErr := fmt.Errorf("wrapped error: %w", baseErr)
readCloser := api.CreateAPIErrReader(wrappedErr)
defer readCloser.Close()
// Read and unmarshal the JSON output
data, err := io.ReadAll(readCloser)
if err != nil {
t.Fatalf("Failed to read from ReadCloser: %v", err)
}
fmt.Println(string(data))
var apiErr api.Error
err = json.Unmarshal(data, &apiErr)
if err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Verify the structure of the APIError
if apiErr.Success {
t.Errorf("Expected Success to be false, got true")
}
if apiErr.Error.Message != wrappedErr.Error() {
t.Errorf("Expected error message to be '%v', got '%v'", wrappedErr.Error(), apiErr.Error.Message)
}
}
func TestCreateAPIErrReader2(t *testing.T) {
_, baseErr := url.Parse("://this is an invalid url")
readCloser := api.CreateAPIErrReader(baseErr)
defer readCloser.Close()
// Read and unmarshal the JSON output
data, err := io.ReadAll(readCloser)
if err != nil {
t.Fatalf("Failed to read from ReadCloser: %v", err)
}
fmt.Println(string(data))
var apiErr api.Error
err = json.Unmarshal(data, &apiErr)
if err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Verify the structure of the APIError
if apiErr.Success {
t.Errorf("Expected Success to be false, got true")
}
if apiErr.Error.Message != baseErr.Error() {
t.Errorf("Expected error message to be '%v', got '%v'", baseErr.Error(), apiErr.Error.Message)
}
}

View File

@@ -0,0 +1,43 @@
package responsemodifiers
import (
_ "embed"
"strings"
"github.com/everywall/ladder/proxychain/responsemodifiers/rewriters"
"github.com/everywall/ladder/proxychain"
)
//go:embed vendor/block_element_removal.js
var blockElementRemoval string
// BlockElementRemoval prevents paywall javascript from removing a
// particular element by detecting the removal, then immediately reinserting it.
// This is useful when a page will return a "fake" 404, after flashing the content briefly.
// If the /outline/ API works, but the regular API doesn't, try this modifier.
func BlockElementRemoval(cssSelector string) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
params := map[string]string{
// ie: "div.article-content"
"{{CSS_SELECTOR}}": cssSelector,
}
rr := rewriters.NewScriptInjectorRewriterWithParams(
blockElementRemoval,
rewriters.BeforeDOMContentLoaded,
params,
)
htmlRewriter := rewriters.NewHTMLRewriter(chain.Response.Body, rr)
chain.Response.Body = htmlRewriter
return nil
}
}

View File

@@ -0,0 +1,34 @@
package responsemodifiers
import (
_ "embed"
"fmt"
"strings"
"github.com/everywall/ladder/proxychain/responsemodifiers/rewriters"
"github.com/everywall/ladder/proxychain"
)
// BlockThirdPartyScripts rewrites HTML and injects JS to block all third party JS from loading.
func BlockThirdPartyScripts() proxychain.ResponseModification {
// TODO: monkey patch fetch and XMLHttpRequest to firewall 3P JS as well.
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
// proxyURL is the URL of the ladder: http://localhost:8080 (ladder)
originalURI := chain.Context.Request().URI()
proxyURL := fmt.Sprintf("%s://%s", originalURI.Scheme(), originalURI.Host())
// replace http.Response.Body with a readcloser that wraps the original, modifying the html attributes
rr := rewriters.NewBlockThirdPartyScriptsRewriter(chain.Request.URL, proxyURL)
blockJSRewriter := rewriters.NewHTMLRewriter(chain.Response.Body, rr)
chain.Response.Body = blockJSRewriter
return nil
}
}

View File

@@ -0,0 +1,21 @@
package responsemodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// BypassCORS modifies response headers to prevent the browser
// from enforcing any CORS restrictions. This should run at the end of the chain.
func BypassCORS() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceResponseModifications(
SetResponseHeader("Access-Control-Allow-Origin", "*"),
SetResponseHeader("Access-Control-Expose-Headers", "*"),
SetResponseHeader("Access-Control-Allow-Credentials", "true"),
SetResponseHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH"),
SetResponseHeader("Access-Control-Allow-Headers", "*"),
DeleteResponseHeader("X-Frame-Options"),
)
return nil
}
}

View File

@@ -0,0 +1,30 @@
package responsemodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// TODO: handle edge case where CSP is specified in meta tag:
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
// BypassContentSecurityPolicy modifies response headers to prevent the browser
// from enforcing any CSP restrictions. This should run at the end of the chain.
func BypassContentSecurityPolicy() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
chain.AddOnceResponseModifications(
DeleteResponseHeader("Content-Security-Policy"),
DeleteResponseHeader("Content-Security-Policy-Report-Only"),
DeleteResponseHeader("X-Content-Security-Policy"),
DeleteResponseHeader("X-WebKit-CSP"),
)
return nil
}
}
// SetContentSecurityPolicy modifies response headers to a specific CSP
func SetContentSecurityPolicy(csp string) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
chain.Response.Header.Set("Content-Security-Policy", csp)
return nil
}
}

View File

@@ -0,0 +1,28 @@
package responsemodifiers
import (
_ "embed"
"strings"
"github.com/everywall/ladder/proxychain"
)
// DeleteLocalStorageData deletes localstorage cookies.
// If the page works once in a fresh incognito window, but fails
// for subsequent loads, try this response modifier alongside
// DeleteSessionStorageData and DeleteIncomingCookies
func DeleteLocalStorageData() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
chain.AddOnceResponseModifications(
InjectScriptBeforeDOMContentLoaded(`window.sessionStorage.clear()`),
InjectScriptAfterDOMContentLoaded(`window.sessionStorage.clear()`),
)
return nil
}
}

View File

@@ -0,0 +1,28 @@
package responsemodifiers
import (
_ "embed"
"strings"
"github.com/everywall/ladder/proxychain"
)
// DeleteSessionStorageData deletes localstorage cookies.
// If the page works once in a fresh incognito window, but fails
// for subsequent loads, try this response modifier alongside
// DeleteLocalStorageData and DeleteIncomingCookies
func DeleteSessionStorageData() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
chain.AddOnceResponseModifications(
InjectScriptBeforeDOMContentLoaded(`window.sessionStorage.clear()`),
InjectScriptAfterDOMContentLoaded(`window.sessionStorage.clear()`),
)
return nil
}
}

View File

@@ -0,0 +1,53 @@
package responsemodifiers
import (
"fmt"
"net/url"
"strings"
"github.com/everywall/ladder/proxychain"
)
var forwardBlacklist map[string]bool
func init() {
forwardBlacklist = map[string]bool{
"content-length": true,
"content-encoding": true,
"transfer-encoding": true,
"strict-transport-security": true,
"connection": true,
"keep-alive": true,
}
}
// ForwardResponseHeaders forwards the response headers from the upstream server to the client
func ForwardResponseHeaders() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// fmt.Println(chain.Response.Header)
for uname, headers := range chain.Response.Header {
name := strings.ToLower(uname)
if forwardBlacklist[name] {
continue
}
// patch location header to forward to proxy instead
if name == "location" {
u, err := url.Parse(chain.Context.BaseURL())
if err != nil {
return err
}
newLocation := fmt.Sprintf("%s://%s/%s", u.Scheme, u.Host, headers[0])
chain.Context.Set("location", newLocation)
}
// forward headers
for _, value := range headers {
fmt.Println(name, value)
chain.Context.Set(name, value)
}
}
return nil
}
}

View File

@@ -0,0 +1,269 @@
package responsemodifiers
import (
"bytes"
"embed"
"fmt"
"html/template"
"io"
"log"
"math"
"net/url"
"strings"
"time"
"github.com/everywall/ladder/proxychain"
"github.com/markusmobius/go-trafilatura"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
//go:embed vendor/generate_readable_outline.html
var templateFS embed.FS
// GenerateReadableOutline creates an reader-friendly distilled representation of the article.
// This is a reliable way of bypassing soft-paywalled articles, where the content is hidden, but still present in the DOM.
func GenerateReadableOutline() proxychain.ResponseModification {
// get template only once, and resuse for subsequent calls
f := "vendor/generate_readable_outline.html"
tmpl, err := template.ParseFS(templateFS, f)
if err != nil {
panic(fmt.Errorf("tx.GenerateReadableOutline Error: %s not found", f))
}
return func(chain *proxychain.ProxyChain) error {
// ===========================================================
// 1. extract dom contents using reading mode algo
// ===========================================================
opts := trafilatura.Options{
IncludeImages: false,
IncludeLinks: true,
FavorRecall: true,
Deduplicate: true,
FallbackCandidates: nil, // TODO: https://github.com/markusmobius/go-trafilatura/blob/main/examples/chained/main.go
// implement fallbacks from "github.com/markusmobius/go-domdistiller" and "github.com/go-shiori/go-readability"
OriginalURL: chain.Request.URL,
}
extract, err := trafilatura.Extract(chain.Response.Body, opts)
if err != nil {
return err
}
// ============================================================================
// 2. render generate_readable_outline.html template using metadata from step 1
// ============================================================================
// render DOM to string without H1 title
removeFirstH1(extract.ContentNode)
// rewrite all links to stay on /outline/ path
rewriteHrefLinks(extract.ContentNode, chain.Context.BaseURL(), chain.APIPrefix)
var b bytes.Buffer
html.Render(&b, extract.ContentNode)
distilledHTML := b.String()
siteName := strings.Split(extract.Metadata.Sitename, ";")[0]
title := strings.Split(extract.Metadata.Title, "|")[0]
fmtDate := createWikipediaDateLink(extract.Metadata.Date)
readingTime := formatDuration(estimateReadingTime(extract.ContentText))
// populate template parameters
data := map[string]interface{}{
"Success": true,
"Image": extract.Metadata.Image,
"Description": extract.Metadata.Description,
"Sitename": siteName,
"Hostname": extract.Metadata.Hostname,
"Url": "/" + chain.Request.URL.String(),
"Title": title,
"Date": fmtDate,
"Author": createDDGFeelingLuckyLinks(extract.Metadata.Author, extract.Metadata.Hostname),
"Body": distilledHTML,
"ReadingTime": readingTime,
}
// ============================================================================
// 3. queue sending the response back to the client by replacing the response body
// (the response body will be read as a stream in proxychain.Execute() later on.)
// ============================================================================
pr, pw := io.Pipe() // pipe io.writer contents into io.reader
// Use a goroutine for writing to the pipe so we don't deadlock the request
go func() {
defer pw.Close()
err := tmpl.Execute(pw, data) // <- render template
if err != nil {
log.Printf("WARN: GenerateReadableOutline template rendering error: %s\n", err)
}
}()
chain.Context.Set("content-type", "text/html")
chain.Response.Body = pr // <- replace response body reader with our new reader from pipe
return nil
}
}
// =============================================
// DOM Rendering helpers
// =============================================
func removeFirstH1(n *html.Node) {
var recurse func(*html.Node) bool
recurse = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.DataAtom == atom.H1 {
return true // Found the first H1, return true to stop
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if recurse(c) {
n.RemoveChild(c)
return false // Removed first H1, no need to continue
}
}
return false
}
recurse(n)
}
func rewriteHrefLinks(n *html.Node, baseURL string, apiPath string) {
u, err := url.Parse(baseURL)
if err != nil {
log.Printf("GenerateReadableOutline :: rewriteHrefLinks error - %s\n", err)
}
apiPath = strings.Trim(apiPath, "/")
proxyURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
newProxyURL := fmt.Sprintf("%s/%s", proxyURL, apiPath)
var recurse func(*html.Node) bool
recurse = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.DataAtom == atom.A {
for i := range n.Attr {
attr := n.Attr[i]
if attr.Key != "href" {
continue
}
// rewrite url on a.href: http://localhost:8080/https://example.com -> http://localhost:8080/outline/https://example.com
attr.Val = strings.Replace(attr.Val, proxyURL, newProxyURL, 1)
// rewrite relative URLs too
if strings.HasPrefix(attr.Val, "/") {
attr.Val = fmt.Sprintf("/%s%s", apiPath, attr.Val)
}
n.Attr[i].Val = attr.Val
log.Println(attr.Val)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
recurse(c)
}
return false
}
recurse(n)
}
// createWikipediaDateLink takes in a date
// and returns an <a> link pointing to the current events page for that day
func createWikipediaDateLink(t time.Time) string {
url := fmt.Sprintf("https://en.wikipedia.org/wiki/Portal:Current_events#%s", t.Format("2006_January_02"))
date := t.Format("January 02, 2006")
return fmt.Sprintf("<a rel=\"noreferrer\" href=\"%s\">%s</a>", url, date)
}
// createDDGFeelingLuckyLinks takes in comma or semicolon separated terms,
// then turns them into <a> links searching for the term using DuckDuckGo's I'm
// feeling lucky feature. It will redirect the user immediately to the first search result.
func createDDGFeelingLuckyLinks(searchTerms string, siteHostname string) string {
siteHostname = strings.TrimSpace(siteHostname)
semiColonSplit := strings.Split(searchTerms, ";")
var links []string
for i, termGroup := range semiColonSplit {
commaSplit := strings.Split(termGroup, ",")
for _, term := range commaSplit {
trimmedTerm := strings.TrimSpace(term)
if trimmedTerm == "" {
continue
}
ddgQuery := fmt.Sprintf(` site:%s intitle:"%s"`, strings.TrimPrefix(siteHostname, "www."), trimmedTerm)
encodedTerm := `\%s:` + url.QueryEscape(ddgQuery)
//ddgURL := `https://html.duckduckgo.com/html/?q=` + encodedTerm
ddgURL := `https://www.duckduckgo.com/?q=` + encodedTerm
link := fmt.Sprintf("<a rel=\"noreferrer\" href=\"%s\">%s</a>", ddgURL, trimmedTerm)
links = append(links, link)
}
// If it's not the last element in semiColonSplit, add a comma to the last link
if i < len(semiColonSplit)-1 {
links[len(links)-1] = links[len(links)-1] + ","
}
}
return strings.Join(links, " ")
}
// estimateReadingTime estimates how long the given text will take to read using the given configuration.
func estimateReadingTime(text string) time.Duration {
if len(text) == 0 {
return 0
}
// Init options with default values.
WordsPerMinute := 200
WordBound := func(b byte) bool {
return b == ' ' || b == '\n' || b == '\r' || b == '\t'
}
words := 0
start := 0
end := len(text) - 1
// Fetch bounds.
for WordBound(text[start]) {
start++
}
for WordBound(text[end]) {
end--
}
// Calculate the number of words.
for i := start; i <= end; {
for i <= end && !WordBound(text[i]) {
i++
}
words++
for i <= end && WordBound(text[i]) {
i++
}
}
// Reading time stats.
minutes := math.Ceil(float64(words) / float64(WordsPerMinute))
duration := time.Duration(math.Ceil(minutes) * float64(time.Minute))
return duration
}
func formatDuration(d time.Duration) string {
// Check if the duration is less than one minute
if d < time.Minute {
seconds := int(d.Seconds())
return fmt.Sprintf("%d seconds", seconds)
}
// Convert the duration to minutes
minutes := int(d.Minutes())
// Format the string for one or more minutes
if minutes == 1 {
return "1 minute"
} else {
return fmt.Sprintf("%d minutes", minutes)
}
}

View File

@@ -0,0 +1,42 @@
package responsemodifiers
import (
_ "embed"
"strings"
"github.com/everywall/ladder/proxychain/responsemodifiers/rewriters"
"github.com/everywall/ladder/proxychain"
)
// injectScript modifies HTTP responses
// to execute javascript at a particular time.
func injectScript(js string, execTime rewriters.ScriptExecTime) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
rr := rewriters.NewScriptInjectorRewriter(js, execTime)
htmlRewriter := rewriters.NewHTMLRewriter(chain.Response.Body, rr)
chain.Response.Body = htmlRewriter
return nil
}
}
// InjectScriptBeforeDOMContentLoaded modifies HTTP responses to inject a JS before DOM Content is loaded (script tag in head)
func InjectScriptBeforeDOMContentLoaded(js string) proxychain.ResponseModification {
return injectScript(js, rewriters.BeforeDOMContentLoaded)
}
// InjectScriptAfterDOMContentLoaded modifies HTTP responses to inject a JS after DOM Content is loaded (script tag in head)
func InjectScriptAfterDOMContentLoaded(js string) proxychain.ResponseModification {
return injectScript(js, rewriters.AfterDOMContentLoaded)
}
// InjectScriptAfterDOMIdle modifies HTTP responses to inject a JS after the DOM is idle (ie: js framework loaded)
func InjectScriptAfterDOMIdle(js string) proxychain.ResponseModification {
return injectScript(js, rewriters.AfterDOMIdle)
}

View File

@@ -0,0 +1,111 @@
package responsemodifiers
import (
"fmt"
http "github.com/bogdanfinn/fhttp"
//"net/http"
//http "github.com/Danny-Dasilva/fhttp"
"github.com/everywall/ladder/proxychain"
)
// DeleteIncomingCookies prevents ALL cookies from being sent from the proxy server
// back down to the client.
func DeleteIncomingCookies(_ ...string) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
chain.Response.Header.Del("Set-Cookie")
chain.AddOnceResponseModifications(
InjectScriptBeforeDOMContentLoaded(`document.cookie = ""`),
InjectScriptAfterDOMContentLoaded(`document.cookie = ""`),
)
return nil
}
}
// DeleteIncomingCookiesExcept prevents non-whitelisted cookies from being sent from the proxy server
// to the client. Cookies whose names are in the whitelist are not removed.
func DeleteIncomingCookiesExcept(whitelist ...string) proxychain.ResponseModification {
return func(px *proxychain.ProxyChain) error {
// Convert whitelist slice to a map for efficient lookups
whitelistMap := make(map[string]struct{})
for _, cookieName := range whitelist {
whitelistMap[cookieName] = struct{}{}
}
// If the response has no cookies, return early
if px.Response.Header == nil {
return nil
}
// Filter the cookies in the response
filteredCookies := []string{}
for _, cookieStr := range px.Response.Header["Set-Cookie"] {
cookie := parseCookie(cookieStr)
if _, found := whitelistMap[cookie.Name]; found {
filteredCookies = append(filteredCookies, cookieStr)
}
}
// Update the Set-Cookie header with the filtered cookies
if len(filteredCookies) > 0 {
px.Response.Header["Set-Cookie"] = filteredCookies
} else {
px.Response.Header.Del("Set-Cookie")
}
return nil
}
}
// parseCookie parses a cookie string and returns an http.Cookie object.
func parseCookie(cookieStr string) *http.Cookie {
header := http.Header{}
header.Add("Set-Cookie", cookieStr)
request := http.Request{Header: header}
return request.Cookies()[0]
}
// SetIncomingCookies adds a raw cookie string being sent from the proxy server down to the client
func SetIncomingCookies(cookies string) proxychain.ResponseModification {
return func(px *proxychain.ProxyChain) error {
px.Response.Header.Set("Set-Cookie", cookies)
return nil
}
}
// SetIncomingCookie modifies a specific cookie in the response from the proxy server to the client.
func SetIncomingCookie(name string, val string) proxychain.ResponseModification {
return func(px *proxychain.ProxyChain) error {
if px.Response.Header == nil {
return nil
}
updatedCookies := []string{}
found := false
// Iterate over existing cookies and modify the one that matches the cookieName
for _, cookieStr := range px.Response.Header["Set-Cookie"] {
cookie := parseCookie(cookieStr)
if cookie.Name == name {
// Replace the cookie with the new value
updatedCookies = append(updatedCookies, fmt.Sprintf("%s=%s", name, val))
found = true
} else {
// Keep the cookie as is
updatedCookies = append(updatedCookies, cookieStr)
}
}
// If the specified cookie wasn't found, add it
if !found {
updatedCookies = append(updatedCookies, fmt.Sprintf("%s=%s", name, val))
}
// Update the Set-Cookie header
px.Response.Header["Set-Cookie"] = updatedCookies
return nil
}
}

View File

@@ -0,0 +1,53 @@
package responsemodifiers
import (
"bytes"
"io"
"regexp"
"strings"
"github.com/everywall/ladder/proxychain"
)
// ModifyIncomingScriptsWithRegex modifies all incoming javascript (application/javascript and inline <script> in text/html) using a regex match and replacement.
func ModifyIncomingScriptsWithRegex(matchRegex string, replacement string) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
path := chain.Request.URL.Path
ct := chain.Response.Header.Get("content-type")
isJavascript := strings.HasSuffix(path, ".js") || ct == "text/javascript" || ct == "application/javascript"
isHTML := strings.HasSuffix(chain.Request.URL.Path, ".html") || ct == "text/html"
switch {
case isJavascript:
rBody, err := modifyResponse(chain.Response.Body, matchRegex, replacement)
if err != nil {
return err
}
chain.Response.Body = rBody
case isHTML:
default:
return nil
}
return nil
}
}
func modifyResponse(body io.ReadCloser, matchRegex, replacement string) (io.ReadCloser, error) {
content, err := io.ReadAll(body)
if err != nil {
return nil, err
}
re, err := regexp.Compile(matchRegex)
if err != nil {
return nil, err
}
err = body.Close()
if err != nil {
return body, err
}
modifiedContent := re.ReplaceAll(content, []byte(replacement))
return io.NopCloser(bytes.NewReader(modifiedContent)), nil
}

View File

@@ -0,0 +1,21 @@
package responsemodifiers
import (
"github.com/everywall/ladder/proxychain"
)
// SetResponseHeader modifies response headers from the upstream server
func SetResponseHeader(key string, value string) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
chain.Context.Set(key, value)
return nil
}
}
// DeleteResponseHeader removes response headers from the upstream server
func DeleteResponseHeader(key string) proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
chain.Context.Response().Header.Del(key)
return nil
}
}

View File

@@ -0,0 +1,70 @@
package responsemodifiers
import (
_ "embed"
"fmt"
"strings"
"github.com/everywall/ladder/proxychain/responsemodifiers/rewriters"
"github.com/everywall/ladder/proxychain"
)
//go:embed vendor/patch_dynamic_resource_urls.js
var patchDynamicResourceURLsScript string
// PatchDynamicResourceURLs patches the javascript runtime to rewrite URLs client-side.
// - This function is designed to allow the proxified page
// to still be browsible by routing all resource URLs through the proxy.
// - Native APIs capable of network requests will be hooked
// and the URLs arguments modified to point to the proxy instead.
// - fetch('/relative_path') -> fetch('/https://proxiedsite.com/relative_path')
// - Element.setAttribute('src', "/assets/img.jpg") -> Element.setAttribute('src', "/https://proxiedsite.com/assets/img.jpg") -> fetch('/https://proxiedsite.com/relative_path')
func PatchDynamicResourceURLs() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
// this is the original URL sent by client:
// http://localhost:8080/http://proxiedsite.com/foo/bar
originalURI := chain.Context.Request().URI()
// this is the extracted URL that the client requests to proxy
// http://proxiedsite.com/foo/bar
reqURL := chain.Request.URL
params := map[string]string{
// ie: http://localhost:8080
"{{PROXY_ORIGIN}}": fmt.Sprintf("%s://%s", originalURI.Scheme(), originalURI.Host()),
// ie: http://proxiedsite.com
"{{ORIGIN}}": fmt.Sprintf("%s://%s", reqURL.Scheme, reqURL.Host),
}
rr := rewriters.NewScriptInjectorRewriterWithParams(
patchDynamicResourceURLsScript,
rewriters.BeforeDOMContentLoaded,
params,
)
htmlRewriter := rewriters.NewHTMLRewriter(chain.Response.Body, rr)
chain.Response.Body = htmlRewriter
// window.location
/*
spoofedLocationAPI := fmt.Sprintf(`{href:"%s", origin:"%s", pathname:"%s", protocol:"%s:", port:"%s"}`,
reqURL.String(), reqURL.Host,
reqURL.Path, reqURL.Scheme, reqURL.Port())
spoofedLocationAPI := fmt.Sprintf(`{origin: "%s"}`, reqURL.Host)
fmt.Println(spoofedLocationAPI)
chain.AddOnceResponseModifications(
ModifyIncomingScriptsWithRegex(`window\.location`, spoofedLocationAPI),
)
*/
return nil
}
}

View File

@@ -0,0 +1,101 @@
package responsemodifiers
import (
"embed"
"encoding/json"
"io"
"log"
"regexp"
"github.com/everywall/ladder/proxychain"
)
//go:embed vendor/ddg-tracker-surrogates/mapping.json
var mappingJSON []byte
//go:embed vendor/ddg-tracker-surrogates/surrogates/*
var surrogateFS embed.FS
var rules domainRules
func init() {
err := json.Unmarshal([]byte(mappingJSON), &rules)
if err != nil {
log.Printf("[ERROR]: PatchTrackerScripts: failed to deserialize ladder/proxychain/responsemodifiers/vendor/ddg-tracker-surrogates/mapping.json")
}
}
// mapping.json schema
type rule struct {
RegexRule *regexp.Regexp `json:"regexRule"`
Surrogate string `json:"surrogate"`
Action string `json:"action,omitempty"`
}
type domainRules map[string][]rule
func (r *rule) UnmarshalJSON(data []byte) error {
type Tmp struct {
RegexRule string `json:"regexRule"`
Surrogate string `json:"surrogate"`
Action string `json:"action,omitempty"`
}
var tmp Tmp
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
regex := regexp.MustCompile(tmp.RegexRule)
r.RegexRule = regex
r.Surrogate = tmp.Surrogate
r.Action = tmp.Action
return nil
}
// PatchTrackerScripts replaces any request to tracker scripts such as google analytics
// with a no-op stub that mocks the API structure of the original scripts they replace.
// Some pages depend on the existence of these structures for proper loading, so this may fix
// some broken elements.
// Surrogate script code borrowed from: DuckDuckGo Privacy Essentials browser extension for Firefox, Chrome. (Apache 2.0 license)
func PatchTrackerScripts() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// preflight checks
reqURL := chain.Request.URL.String()
isTracker := false
//
var surrogateScript io.ReadCloser
for domain, domainRules := range rules {
for _, rule := range domainRules {
if !rule.RegexRule.MatchString(reqURL) {
continue
}
// found tracker script, replacing response body with nop stub from
// ./vendor/ddg-tracker-surrogates/surrogates/{{rule.Surrogate}}
isTracker = true
script, err := surrogateFS.Open("vendor/ddg-tracker-surrogates/surrogates/" + rule.Surrogate)
if err != nil {
panic(err)
}
surrogateScript = io.NopCloser(script)
log.Printf("INFO: PatchTrackerScripts :: injecting surrogate for '%s' => 'surrogates/%s'\n", domain, rule.Surrogate)
break
}
}
if !isTracker {
return nil
}
chain.Response.Body = surrogateScript
chain.Context.Set("content-type", "text/javascript")
return nil
}
}

View File

@@ -0,0 +1,37 @@
package responsemodifiers
import (
_ "embed"
"fmt"
"strings"
"github.com/everywall/ladder/proxychain/responsemodifiers/rewriters"
"github.com/everywall/ladder/proxychain"
)
// RewriteHTMLResourceURLs modifies HTTP responses
// to rewrite URLs attributes in HTML content (such as src, href)
// - `<img src='/relative_path'>` -> `<img src='/https://proxiedsite.com/relative_path'>`
// - This function is designed to allow the proxified page
// to still be browsible by routing all resource URLs through the proxy.
func RewriteHTMLResourceURLs() proxychain.ResponseModification {
return func(chain *proxychain.ProxyChain) error {
// don't add rewriter if it's not even html
ct := chain.Response.Header.Get("content-type")
if !strings.HasPrefix(ct, "text/html") {
return nil
}
// proxyURL is the URL of the ladder: http://localhost:8080 (ladder)
originalURI := chain.Context.Request().URI()
proxyURL := fmt.Sprintf("%s://%s", originalURI.Scheme(), originalURI.Host())
// replace http.Response.Body with a readcloser that wraps the original, modifying the html attributes
rr := rewriters.NewHTMLTokenURLRewriter(chain.Request.URL, proxyURL)
htmlRewriter := rewriters.NewHTMLRewriter(chain.Response.Body, rr)
chain.Response.Body = htmlRewriter
return nil
}
}

Some files were not shown because too many files have changed in this diff Show More