Browse Source
feat: Improve stability and performance for automatic captcha solver, fallback to manual captchapull/120/head v1.5.0
committed by
GitHub
10 changed files with 1772 additions and 452 deletions
@ -0,0 +1,28 @@ |
|||||
|
name: "Close Stale Issues" |
||||
|
on: |
||||
|
schedule: |
||||
|
- cron: "0 0 * * 3" |
||||
|
workflow_dispatch: |
||||
|
|
||||
|
jobs: |
||||
|
stale: |
||||
|
permissions: |
||||
|
issues: write |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10 |
||||
|
with: |
||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }} |
||||
|
stale-issue-message: > |
||||
|
Привет! 👋 |
||||
|
<br><br> |
||||
|
Данный issue давно не обновлялся. Если в течение недели здесь не будет больше активности, issue будет закрыт. |
||||
|
<br><br> |
||||
|
Спасибо! |
||||
|
close-issue-message: "Issue был закрыт из-за отсутствия активности." |
||||
|
days-before-stale: 120 |
||||
|
days-before-close: 7 |
||||
|
operations-per-run: 1000 |
||||
|
ascending: true |
||||
|
enable-statistics: true |
||||
|
stale-issue-label: "Stale" |
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
@ -0,0 +1,578 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"compress/gzip" |
||||
|
"context" |
||||
|
"encoding/json" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"log" |
||||
|
"net" |
||||
|
"net/http" |
||||
|
"net/http/httputil" |
||||
|
neturl "net/url" |
||||
|
"os/exec" |
||||
|
"runtime" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/bschaatsbergen/dnsdialer" |
||||
|
) |
||||
|
|
||||
|
const captchaListenPort = "8765" |
||||
|
|
||||
|
type browserCommand struct { |
||||
|
name string |
||||
|
args []string |
||||
|
} |
||||
|
|
||||
|
func localCaptchaOrigin() string { |
||||
|
return "http://localhost:" + captchaListenPort |
||||
|
} |
||||
|
|
||||
|
func localCaptchaListenAddrs() []string { |
||||
|
return []string{ |
||||
|
"127.0.0.1:" + captchaListenPort, |
||||
|
"[::1]:" + captchaListenPort, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func localCaptchaHosts() []string { |
||||
|
return []string{ |
||||
|
"localhost:" + captchaListenPort, |
||||
|
"127.0.0.1:" + captchaListenPort, |
||||
|
"[::1]:" + captchaListenPort, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func isLocalCaptchaHost(host string) bool { |
||||
|
for _, localHost := range localCaptchaHosts() { |
||||
|
if strings.EqualFold(host, localHost) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func localCaptchaURLForTarget(targetURL *neturl.URL) string { |
||||
|
localURL := &neturl.URL{ |
||||
|
Scheme: "http", |
||||
|
Host: "localhost:" + captchaListenPort, |
||||
|
Path: targetURL.Path, |
||||
|
RawPath: targetURL.RawPath, |
||||
|
RawQuery: targetURL.RawQuery, |
||||
|
} |
||||
|
if localURL.Path == "" { |
||||
|
localURL.Path = "/" |
||||
|
} |
||||
|
return localURL.String() |
||||
|
} |
||||
|
|
||||
|
func targetOrigin(targetURL *neturl.URL) string { |
||||
|
return targetURL.Scheme + "://" + targetURL.Host |
||||
|
} |
||||
|
|
||||
|
func rewriteProxyHeaderURL(raw string, targetURL *neturl.URL) string { |
||||
|
if raw == "" { |
||||
|
return raw |
||||
|
} |
||||
|
parsed, err := neturl.Parse(raw) |
||||
|
if err != nil { |
||||
|
return raw |
||||
|
} |
||||
|
if parsed.Scheme != "http" || !isLocalCaptchaHost(parsed.Host) { |
||||
|
return raw |
||||
|
} |
||||
|
parsed.Scheme = targetURL.Scheme |
||||
|
parsed.Host = targetURL.Host |
||||
|
return parsed.String() |
||||
|
} |
||||
|
|
||||
|
func rewriteProxyRequest(req *http.Request, targetURL *neturl.URL) { |
||||
|
req.URL.Scheme = targetURL.Scheme |
||||
|
req.URL.Host = targetURL.Host |
||||
|
if req.URL.Path == "" { |
||||
|
req.URL.Path = targetURL.Path |
||||
|
} |
||||
|
req.Host = targetURL.Host |
||||
|
|
||||
|
req.Header.Del("Accept-Encoding") |
||||
|
for _, headerName := range []string{"Origin", "Referer"} { |
||||
|
if rewritten := rewriteProxyHeaderURL(req.Header.Get(headerName), targetURL); rewritten != "" { |
||||
|
req.Header.Set(headerName, rewritten) |
||||
|
} else { |
||||
|
req.Header.Del(headerName) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func extractSuccessToken(body []byte) string { |
||||
|
var payload struct { |
||||
|
Response struct { |
||||
|
SuccessToken string `json:"success_token"` |
||||
|
} `json:"response"` |
||||
|
} |
||||
|
if err := json.Unmarshal(body, &payload); err != nil { |
||||
|
return "" |
||||
|
} |
||||
|
return payload.Response.SuccessToken |
||||
|
} |
||||
|
|
||||
|
func rewriteProxyCookies(header http.Header) { |
||||
|
cookies := (&http.Response{Header: header}).Cookies() |
||||
|
if len(cookies) == 0 { |
||||
|
return |
||||
|
} |
||||
|
header.Del("Set-Cookie") |
||||
|
for _, cookie := range cookies { |
||||
|
cookie.Domain = "" |
||||
|
cookie.Secure = false |
||||
|
cookie.Partitioned = false |
||||
|
if cookie.SameSite == http.SameSiteNoneMode || cookie.SameSite == http.SameSiteStrictMode { |
||||
|
cookie.SameSite = http.SameSiteLaxMode |
||||
|
} |
||||
|
header.Add("Set-Cookie", cookie.String()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string { |
||||
|
localOrigin := localCaptchaOrigin() |
||||
|
upstreamOrigin := targetOrigin(targetURL) |
||||
|
html = strings.ReplaceAll(html, upstreamOrigin, localOrigin) |
||||
|
|
||||
|
script := fmt.Sprintf(` |
||||
|
<script> |
||||
|
(function() { |
||||
|
var localOrigin = %q; |
||||
|
var upstreamOrigin = %q; |
||||
|
|
||||
|
function rewriteUrl(urlStr) { |
||||
|
if (!urlStr || typeof urlStr !== 'string') return urlStr; |
||||
|
if (urlStr.indexOf(localOrigin) === 0) return urlStr; |
||||
|
if (urlStr.indexOf(upstreamOrigin) === 0) return localOrigin + urlStr.slice(upstreamOrigin.length); |
||||
|
if (urlStr.indexOf('//') === 0) {
|
||||
|
return '/generic_proxy?proxy_url=' + encodeURIComponent(window.location.protocol + urlStr); |
||||
|
} |
||||
|
if (urlStr.indexOf('http://') === 0 || urlStr.indexOf('https://') === 0) {
|
||||
|
return '/generic_proxy?proxy_url=' + encodeURIComponent(urlStr); |
||||
|
} |
||||
|
return urlStr; |
||||
|
} |
||||
|
|
||||
|
function rewriteElementAttr(el, attr) { |
||||
|
if (!el || !el.getAttribute) return; |
||||
|
var value = el.getAttribute(attr); |
||||
|
if (!value) return; |
||||
|
var rewritten = rewriteUrl(value); |
||||
|
if (rewritten !== value) { |
||||
|
el.setAttribute(attr, rewritten); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function rewriteDocument(root) { |
||||
|
if (!root || !root.querySelectorAll) return; |
||||
|
root.querySelectorAll('[href]').forEach(function(el) { rewriteElementAttr(el, 'href'); }); |
||||
|
root.querySelectorAll('[src]').forEach(function(el) { rewriteElementAttr(el, 'src'); }); |
||||
|
root.querySelectorAll('form[action]').forEach(function(el) { rewriteElementAttr(el, 'action'); }); |
||||
|
} |
||||
|
|
||||
|
function handleSuccessToken(token) { |
||||
|
if (!token) return; |
||||
|
fetch('/local-captcha-result', { |
||||
|
method: 'POST', |
||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'}, |
||||
|
body: 'token=' + encodeURIComponent(token) |
||||
|
}).then(function() { |
||||
|
document.body.innerHTML = '<h2 style="text-align:center;margin-top:20vh">Done! You can close the page.</h2>'; |
||||
|
setTimeout(function() { window.close(); }, 300); |
||||
|
}).catch(function() {}); |
||||
|
} |
||||
|
|
||||
|
var origOpen = XMLHttpRequest.prototype.open; |
||||
|
XMLHttpRequest.prototype.open = function() { |
||||
|
if (arguments[1] && typeof arguments[1] === 'string') { |
||||
|
this._origUrl = arguments[1]; |
||||
|
arguments[1] = rewriteUrl(arguments[1]); |
||||
|
} |
||||
|
return origOpen.apply(this, arguments); |
||||
|
}; |
||||
|
|
||||
|
var origSend = XMLHttpRequest.prototype.send; |
||||
|
XMLHttpRequest.prototype.send = function() { |
||||
|
var xhr = this; |
||||
|
if (this._origUrl && this._origUrl.indexOf('captchaNotRobot.check') !== -1) { |
||||
|
xhr.addEventListener('load', function() { |
||||
|
try { |
||||
|
var data = JSON.parse(xhr.responseText); |
||||
|
if (data.response && data.response.success_token) { |
||||
|
handleSuccessToken(data.response.success_token); |
||||
|
} |
||||
|
} catch (e) {} |
||||
|
}); |
||||
|
} |
||||
|
return origSend.apply(this, arguments); |
||||
|
}; |
||||
|
|
||||
|
var origFetch = window.fetch; |
||||
|
if (origFetch) { |
||||
|
window.fetch = function() { |
||||
|
var url = arguments[0]; |
||||
|
var isObj = (typeof url === 'object' && url && url.url); |
||||
|
var urlStr = isObj ? url.url : url; |
||||
|
var origUrlStr = urlStr; |
||||
|
|
||||
|
if (typeof urlStr === 'string') { |
||||
|
urlStr = rewriteUrl(urlStr); |
||||
|
arguments[0] = urlStr; |
||||
|
} |
||||
|
|
||||
|
var p = origFetch.apply(this, arguments); |
||||
|
if (typeof origUrlStr === 'string' && origUrlStr.indexOf('captchaNotRobot.check') !== -1) { |
||||
|
p.then(function(response) { |
||||
|
return response.clone().json(); |
||||
|
}).then(function(data) { |
||||
|
if (data.response && data.response.success_token) { |
||||
|
handleSuccessToken(data.response.success_token); |
||||
|
} |
||||
|
}).catch(function() {}); |
||||
|
} |
||||
|
return p; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
document.addEventListener('submit', function(event) { |
||||
|
if (event.target && event.target.action) { |
||||
|
event.target.action = rewriteUrl(event.target.action); |
||||
|
} |
||||
|
}, true); |
||||
|
|
||||
|
document.addEventListener('click', function(event) { |
||||
|
var target = event.target && event.target.closest ? event.target.closest('a[href]') : null; |
||||
|
if (target && target.href) { |
||||
|
target.href = rewriteUrl(target.href); |
||||
|
} |
||||
|
}, true); |
||||
|
|
||||
|
var origFormSubmit = HTMLFormElement.prototype.submit; |
||||
|
HTMLFormElement.prototype.submit = function() { |
||||
|
if (this.action) { |
||||
|
this.action = rewriteUrl(this.action); |
||||
|
} |
||||
|
return origFormSubmit.apply(this, arguments); |
||||
|
}; |
||||
|
|
||||
|
var origWindowOpen = window.open; |
||||
|
if (origWindowOpen) { |
||||
|
window.open = function(url) { |
||||
|
if (typeof url === 'string') { |
||||
|
arguments[0] = rewriteUrl(url); |
||||
|
} |
||||
|
return origWindowOpen.apply(this, arguments); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
rewriteDocument(document); |
||||
|
if (document.documentElement && window.MutationObserver) { |
||||
|
new MutationObserver(function(mutations) { |
||||
|
mutations.forEach(function(mutation) { |
||||
|
if (mutation.type === 'attributes' && mutation.target) { |
||||
|
rewriteElementAttr(mutation.target, mutation.attributeName); |
||||
|
return; |
||||
|
} |
||||
|
mutation.addedNodes.forEach(function(node) { |
||||
|
if (node.nodeType === 1) { |
||||
|
rewriteDocument(node); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
}).observe(document.documentElement, { |
||||
|
subtree: true, |
||||
|
childList: true, |
||||
|
attributes: true, |
||||
|
attributeFilter: ['href', 'src', 'action'] |
||||
|
}); |
||||
|
} |
||||
|
})(); |
||||
|
</script> |
||||
|
`, localOrigin, upstreamOrigin) |
||||
|
|
||||
|
switch { |
||||
|
case strings.Contains(html, "</head>"): |
||||
|
return strings.Replace(html, "</head>", script+"</head>", 1) |
||||
|
case strings.Contains(html, "</body>"): |
||||
|
return strings.Replace(html, "</body>", script+"</body>", 1) |
||||
|
default: |
||||
|
return html + script |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func newCaptchaProxyTransport(dialer *dnsdialer.Dialer) *http.Transport { |
||||
|
transport := &http.Transport{ |
||||
|
MaxIdleConns: 100, |
||||
|
MaxIdleConnsPerHost: 100, |
||||
|
IdleConnTimeout: 90 * time.Second, |
||||
|
TLSHandshakeTimeout: 10 * time.Second, |
||||
|
ExpectContinueTimeout: 1 * time.Second, |
||||
|
ForceAttemptHTTP2: true, |
||||
|
} |
||||
|
if dialer != nil { |
||||
|
transport.DialContext = dialer.DialContext |
||||
|
} |
||||
|
return transport |
||||
|
} |
||||
|
|
||||
|
func startCaptchaServer(srv *http.Server, logPrefix string) error { |
||||
|
var listenErrs []string |
||||
|
var listening bool |
||||
|
|
||||
|
for _, addr := range localCaptchaListenAddrs() { |
||||
|
listener, err := net.Listen("tcp", addr) |
||||
|
if err != nil { |
||||
|
listenErrs = append(listenErrs, fmt.Sprintf("%s (%v)", addr, err)) |
||||
|
continue |
||||
|
} |
||||
|
listening = true |
||||
|
go func(listener net.Listener) { |
||||
|
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { |
||||
|
log.Printf("%s: %s", logPrefix, err) |
||||
|
} |
||||
|
}(listener) |
||||
|
} |
||||
|
|
||||
|
if listening { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return fmt.Errorf("captcha listeners failed: %s", strings.Join(listenErrs, "; ")) |
||||
|
} |
||||
|
|
||||
|
// runCaptchaServerAndWait triggers the browser, and waiting gracefully for the solution token.
|
||||
|
func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-chan string, logPrefix string) (string, error) { |
||||
|
srv := &http.Server{Handler: handler} |
||||
|
|
||||
|
if err := startCaptchaServer(srv, logPrefix); err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
fmt.Println("\n==============================================") |
||||
|
fmt.Println("ACTION REQUIRED: MANUAL CAPTCHA SOLVING NEEDED") |
||||
|
fmt.Println("Open this URL in your browser: " + captchaURL) |
||||
|
fmt.Println("==============================================") |
||||
|
fmt.Println() |
||||
|
openBrowser(captchaURL) |
||||
|
|
||||
|
key := <-keyCh |
||||
|
|
||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
||||
|
defer cancel() |
||||
|
if err := srv.Shutdown(ctx); err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
return key, nil |
||||
|
} |
||||
|
|
||||
|
// notifyKey pushes the key string to the given channel without blocking
|
||||
|
func notifyKey(keyCh chan<- string, key string) { |
||||
|
if key != "" { |
||||
|
select { |
||||
|
case keyCh <- key: |
||||
|
default: |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func solveCaptchaViaHTTP(captchaImg string) (string, error) { |
||||
|
keyCh := make(chan string, 1) |
||||
|
mux := http.NewServeMux() |
||||
|
|
||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
||||
|
_, _ = fmt.Fprintf(w, `<!DOCTYPE html> |
||||
|
<html><head> |
||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"> |
||||
|
<style>body{font-family:sans-serif;text-align:center;padding:20px} |
||||
|
img{max-width:100%%;margin:16px 0} |
||||
|
input{font-size:24px;padding:12px;width:80%%;box-sizing:border-box} |
||||
|
button{font-size:24px;padding:12px 32px;margin-top:12px;cursor:pointer}</style> |
||||
|
</head><body> |
||||
|
<h2>Solve the Captcha</h2> |
||||
|
<img src="%s" alt="captcha"/> |
||||
|
<form onsubmit="fetch('/solve?key='+encodeURIComponent(document.getElementById('k').value)).then(()=>{document.body.innerHTML='<h2>Done!</h2>';setTimeout(function(){window.close();}, 300);});return false;"> |
||||
|
<br><input id="k" type="text" autofocus placeholder="Text from image"/> |
||||
|
<br><button type="submit">Submit</button> |
||||
|
</form></body></html>`, captchaImg) |
||||
|
}) |
||||
|
|
||||
|
mux.HandleFunc("/solve", func(w http.ResponseWriter, r *http.Request) { |
||||
|
notifyKey(keyCh, r.URL.Query().Get("key")) |
||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
||||
|
_, _ = fmt.Fprint(w, `<!DOCTYPE html><html><body><h2>Done!</h2></body></html>`) |
||||
|
}) |
||||
|
|
||||
|
return runCaptchaServerAndWait(mux, localCaptchaOrigin(), keyCh, "captcha HTTP server error") |
||||
|
} |
||||
|
|
||||
|
func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, error) { |
||||
|
keyCh := make(chan string, 1) |
||||
|
|
||||
|
targetURL, err := neturl.Parse(redirectURI) |
||||
|
if err != nil { |
||||
|
return "", fmt.Errorf("invalid redirect URI: %v", err) |
||||
|
} |
||||
|
|
||||
|
transport := newCaptchaProxyTransport(dialer) |
||||
|
|
||||
|
proxy := &httputil.ReverseProxy{ |
||||
|
Transport: transport, |
||||
|
Rewrite: func(req *httputil.ProxyRequest) { |
||||
|
rewriteProxyRequest(req.Out, targetURL) |
||||
|
}, |
||||
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { |
||||
|
log.Printf("captcha proxy error for %s: %v", r.URL.String(), err) |
||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
||||
|
w.WriteHeader(http.StatusBadGateway) |
||||
|
_, _ = fmt.Fprintf(w, `<!DOCTYPE html><html><body style="font-family:sans-serif;padding:20px"><h2>Captcha proxy error</h2><p>%v</p></body></html>`, err) |
||||
|
}, |
||||
|
ModifyResponse: func(res *http.Response) error { |
||||
|
rewriteProxyCookies(res.Header) |
||||
|
|
||||
|
if res.StatusCode >= 300 && res.StatusCode < 400 { |
||||
|
if loc := res.Header.Get("Location"); loc != "" { |
||||
|
if strings.HasPrefix(loc, "/") { |
||||
|
res.Header.Set("Location", loc) |
||||
|
} else if strings.HasPrefix(loc, targetOrigin(targetURL)) { |
||||
|
res.Header.Set("Location", strings.Replace(loc, targetOrigin(targetURL), localCaptchaOrigin(), 1)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
contentType := res.Header.Get("Content-Type") |
||||
|
shouldInspectBody := strings.Contains(contentType, "text/html") || strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") |
||||
|
if !shouldInspectBody { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
reader := res.Body |
||||
|
if res.Header.Get("Content-Encoding") == "gzip" { |
||||
|
gzReader, err := gzip.NewReader(res.Body) |
||||
|
if err == nil { |
||||
|
reader = gzReader |
||||
|
defer func() { |
||||
|
if err := gzReader.Close(); err != nil { |
||||
|
log.Printf("failed to close gzip reader: %v", err) |
||||
|
} |
||||
|
}() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bodyBytes, err := io.ReadAll(reader) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if err := res.Body.Close(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") { |
||||
|
notifyKey(keyCh, extractSuccessToken(bodyBytes)) |
||||
|
} |
||||
|
|
||||
|
if strings.Contains(contentType, "text/html") { |
||||
|
for _, headerName := range []string{ |
||||
|
"Content-Security-Policy", |
||||
|
"Content-Security-Policy-Report-Only", |
||||
|
"X-Content-Security-Policy", |
||||
|
"X-WebKit-CSP", |
||||
|
"Cross-Origin-Opener-Policy", |
||||
|
"Cross-Origin-Embedder-Policy", |
||||
|
"Cross-Origin-Resource-Policy", |
||||
|
"X-Frame-Options", |
||||
|
} { |
||||
|
res.Header.Del(headerName) |
||||
|
} |
||||
|
|
||||
|
bodyBytes = []byte(rewriteCaptchaHTML(string(bodyBytes), targetURL)) |
||||
|
res.Header.Del("Content-Encoding") |
||||
|
} |
||||
|
|
||||
|
res.Body = io.NopCloser(bytes.NewReader(bodyBytes)) |
||||
|
res.ContentLength = int64(len(bodyBytes)) |
||||
|
res.Header.Set("Content-Length", fmt.Sprint(len(bodyBytes))) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
mux := http.NewServeMux() |
||||
|
mux.HandleFunc("/local-captcha-result", func(w http.ResponseWriter, r *http.Request) { |
||||
|
notifyKey(keyCh, r.FormValue("token")) // r.FormValue automatically parses the form
|
||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||
|
_, _ = fmt.Fprint(w, "ok") |
||||
|
}) |
||||
|
|
||||
|
mux.HandleFunc("/generic_proxy", func(w http.ResponseWriter, r *http.Request) { |
||||
|
targetAuthUrl := r.URL.Query().Get("proxy_url") |
||||
|
targetParsed, err := neturl.Parse(targetAuthUrl) |
||||
|
if err != nil || targetParsed.Host == "" { |
||||
|
http.Error(w, "Bad URL", http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
genericReverse := &httputil.ReverseProxy{ |
||||
|
Transport: transport, |
||||
|
Rewrite: func(req *httputil.ProxyRequest) { |
||||
|
req.Out.URL.Path = targetParsed.Path |
||||
|
req.Out.URL.RawQuery = targetParsed.RawQuery |
||||
|
rewriteProxyRequest(req.Out, targetParsed) |
||||
|
}, |
||||
|
} |
||||
|
genericReverse.ServeHTTP(w, r) |
||||
|
}) |
||||
|
|
||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
|
if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" { |
||||
|
http.Redirect(w, r, localCaptchaURLForTarget(targetURL), http.StatusTemporaryRedirect) |
||||
|
return |
||||
|
} |
||||
|
proxy.ServeHTTP(w, r) |
||||
|
}) |
||||
|
|
||||
|
return runCaptchaServerAndWait(mux, localCaptchaURLForTarget(targetURL), keyCh, "proxy HTTP server error") |
||||
|
} |
||||
|
|
||||
|
func openBrowser(url string) { |
||||
|
for _, cmd := range browserOpenCommands(runtime.GOOS, url) { |
||||
|
if err := exec.Command(cmd.name, cmd.args...).Start(); err == nil { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func browserOpenCommands(goos string, url string) []browserCommand { |
||||
|
switch goos { |
||||
|
case "windows": |
||||
|
return []browserCommand{{name: "cmd", args: []string{"/c", "start", url}}} |
||||
|
case "darwin": |
||||
|
return []browserCommand{{name: "open", args: []string{url}}} |
||||
|
case "linux": |
||||
|
return []browserCommand{ |
||||
|
{name: "xdg-open", args: []string{url}}, |
||||
|
{name: "gio", args: []string{"open", url}}, |
||||
|
} |
||||
|
case "android": |
||||
|
return []browserCommand{ |
||||
|
{name: "termux-open-url", args: []string{url}}, |
||||
|
{name: "/system/bin/am", args: []string{"start", "-a", "android.intent.action.VIEW", "-d", url}}, |
||||
|
{name: "am", args: []string{"start", "-a", "android.intent.action.VIEW", "-d", url}}, |
||||
|
{name: "xdg-open", args: []string{url}}, |
||||
|
} |
||||
|
case "ios": |
||||
|
return []browserCommand{ |
||||
|
{name: "open", args: []string{url}}, |
||||
|
{name: "uiopen", args: []string{url}}, |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
Loading…
Reference in new issue