ai/pp
1
0
Fork 0
pp/main.go

240 lines
5.3 KiB
Go

package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync/atomic"
"time"
)
var (
proxyUser string
proxyPass string
proxyHost string
sessionID string
rotateCounter atomic.Int64
)
func getEnvDefault(name, def string) string {
v := os.Getenv(name)
if v != "" {
return v
}
return def
}
func generateSessionID(length int) string {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
log.Fatalf("failed to generate session ID: %v", err)
}
return base64.RawURLEncoding.EncodeToString(b)[:length]
}
func getUpstreamURL() *url.URL {
counter := rotateCounter.Load()
user := fmt.Sprintf("%s%s%d", proxyUser, sessionID, counter)
return &url.URL{
Scheme: "http",
User: url.UserPassword(user, proxyPass),
Host: proxyHost,
}
}
func parseProxyConfig() {
proxyUser = os.Getenv("PROXY_USER")
if proxyUser == "" {
log.Fatal("missing PROXY_USER")
}
proxyPass = os.Getenv("PROXY_PASS")
if proxyPass == "" {
log.Fatal("missing PROXY_PASS")
}
proxyHost = os.Getenv("PROXY_HOST")
if proxyHost == "" {
log.Fatal("missing PROXY_HOST")
}
port := os.Getenv("PROXY_PORT")
if port == "" {
log.Fatal("missing PROXY_PORT")
}
proxyHost = net.JoinHostPort(proxyHost, port)
sessionID = generateSessionID(11)
rotateCounter.Store(0)
}
func getCurrentIP() string {
upstream := getUpstreamURL()
transport := &http.Transport{Proxy: http.ProxyURL(upstream)}
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
resp, err := client.Get("https://checkip.amazonaws.com")
if err != nil {
return fmt.Sprintf("error: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Sprintf("error reading: %v", err)
}
return strings.TrimSpace(string(body))
}
func proxyHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/health":
w.Write([]byte("ok"))
return
case "/rotate":
newCounter := rotateCounter.Add(1)
ip := getCurrentIP()
log.Printf("Rotated to counter=%d, IP=%s", newCounter, ip)
w.Write([]byte(fmt.Sprintf("%s\n", ip)))
return
}
upstream := getUpstreamURL()
transport := &http.Transport{Proxy: http.ProxyURL(upstream)}
if r.Method == http.MethodConnect {
handleTunneling(w, r, upstream)
return
}
handleHTTP(w, r, transport)
}
}
func handleHTTP(w http.ResponseWriter, r *http.Request, transport *http.Transport) {
target := r.URL
if !target.IsAbs() {
target = &(*r.URL)
target.Scheme = "http"
target.Host = r.Host
}
outReq, err := http.NewRequestWithContext(r.Context(), r.Method, target.String(), r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
outReq.Header = r.Header.Clone()
outReq.RequestURI = ""
resp, err := transport.RoundTrip(outReq)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
func handleTunneling(w http.ResponseWriter, r *http.Request, upstream *url.URL) {
targetAddr := r.Host
if _, _, err := net.SplitHostPort(targetAddr); err != nil {
targetAddr = net.JoinHostPort(targetAddr, "443")
}
upstreamConn, err := net.DialTimeout("tcp", upstream.Host, 10*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
if _, err := fmt.Fprintf(
upstreamConn,
"CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: %s\r\n\r\n",
targetAddr,
targetAddr,
basicAuthHeader(upstream.User),
); err != nil {
_ = upstreamConn.Close()
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
br := bufio.NewReader(upstreamConn)
resp, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
if err != nil {
_ = upstreamConn.Close()
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
if resp.StatusCode != http.StatusOK {
_ = resp.Body.Close()
_ = upstreamConn.Close()
http.Error(w, resp.Status, http.StatusBadGateway)
return
}
_ = resp.Body.Close()
hijacker, ok := w.(http.Hijacker)
if !ok {
_ = upstreamConn.Close()
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
_ = upstreamConn.Close()
return
}
_, _ = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
go transfer(upstreamConn, clientConn)
go transfer(clientConn, upstreamConn)
}
func basicAuthHeader(user *url.Userinfo) string {
if user == nil {
return ""
}
password, _ := user.Password()
token := base64.StdEncoding.EncodeToString([]byte(user.Username() + ":" + password))
return "Basic " + token
}
func transfer(dst io.WriteCloser, src io.ReadCloser) {
defer dst.Close()
defer src.Close()
_, _ = io.Copy(dst, src)
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func main() {
listenAddr := ":" + getEnvDefault("PORT", "80")
parseProxyConfig()
upstream := getUpstreamURL()
ip := getCurrentIP()
log.Printf("started listen=%s upstream=http://%s ip=%s", listenAddr, upstream.Host, ip)
log.Fatal(http.ListenAndServe(listenAddr, proxyHandler()))
}