Launch
This commit is contained in:
399
internal/engine/payload.go
Normal file
399
internal/engine/payload.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
payloadHTTPStatusPeekTimeoutMs = 1500
|
||||
payloadHTTPStatusLineLimit = 4096
|
||||
maxPayloadHTTPResponsesToSkip = 12
|
||||
maxPayloadHTTPHeaderLines = 80
|
||||
maxPayloadHTTPBodyDiscardBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
type PayloadResult struct {
|
||||
StatusLine string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
type httpResponseInfo struct {
|
||||
contentLength int64
|
||||
chunked bool
|
||||
}
|
||||
|
||||
type preloadedConn struct {
|
||||
net.Conn
|
||||
preloaded *bytes.Reader
|
||||
}
|
||||
|
||||
func (c *preloadedConn) Read(p []byte) (int, error) {
|
||||
if c.preloaded != nil && c.preloaded.Len() > 0 {
|
||||
return c.preloaded.Read(p)
|
||||
}
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
|
||||
func wrapConnWithPreloadedBytes(conn net.Conn, b []byte) net.Conn {
|
||||
if len(b) == 0 {
|
||||
return conn
|
||||
}
|
||||
return &preloadedConn{Conn: conn, preloaded: bytes.NewReader(b)}
|
||||
}
|
||||
|
||||
func WritePayload(conn net.Conn, p config.Profile, targetHost string, targetPort int, logger *Logger) (PayloadResult, net.Conn, error) {
|
||||
payload := buildPayload(p.Payload.Text, targetHost, targetPort)
|
||||
parts, instant := splitPayload(payload)
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := io.WriteString(conn, part); err != nil {
|
||||
return PayloadResult{}, conn, err
|
||||
}
|
||||
if i < len(parts)-1 && !instant {
|
||||
time.Sleep(time.Duration(p.Payload.SplitDelayMs) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
logger.Add("debug", "payload sent (%d bytes)", len(payload))
|
||||
|
||||
if !p.Payload.WaitForResponse {
|
||||
return PayloadResult{}, conn, nil
|
||||
}
|
||||
|
||||
return consumePayloadHTTPNegotiation(conn, p, payloadSourceLabel(p.Mode), logger)
|
||||
}
|
||||
|
||||
func consumePayloadHTTPNegotiation(conn net.Conn, p config.Profile, source string, logger *Logger) (PayloadResult, net.Conn, error) {
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
|
||||
var last PayloadResult
|
||||
var captured *bytes.Buffer
|
||||
var sawSuccess bool
|
||||
|
||||
for attempt := 0; attempt < maxPayloadHTTPResponsesToSkip; attempt++ {
|
||||
setPayloadReadDeadline(conn, p, attempt)
|
||||
captured = &bytes.Buffer{}
|
||||
line, err := readPayloadLinePreserveBytes(conn, captured, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
if isTimeoutErr(err) {
|
||||
if last.StatusCode >= 400 && !p.Payload.AcceptAnyStatus && !sawSuccess {
|
||||
return last, conn, fmt.Errorf("payload rejected with final status %d", last.StatusCode)
|
||||
}
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
if err == io.EOF && captured.Len() > 0 {
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
if last.StatusCode > 0 {
|
||||
return last, conn, nil
|
||||
}
|
||||
return PayloadResult{}, conn, fmt.Errorf("payload response read failed: %w", err)
|
||||
}
|
||||
|
||||
cleanLine := strings.TrimSpace(line)
|
||||
if cleanLine == "" {
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cleanLine, "SSH-") || !isHTTPStatusLine(cleanLine) {
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
|
||||
code := parseStatusCode(cleanLine)
|
||||
last = PayloadResult{StatusLine: cleanLine, StatusCode: code}
|
||||
logProxyStatus(logger, source, code, cleanLine)
|
||||
logHTTPCompatibilityStatus(logger, code, cleanLine)
|
||||
if code == 101 || (code >= 200 && code < 400) {
|
||||
sawSuccess = true
|
||||
}
|
||||
|
||||
// The current bytes are confirmed HTTP/proxy negotiation bytes. Do not replay
|
||||
// them to the SSH transport. Only replay bytes when we detect SSH/non-HTTP
|
||||
// data or a partial line after timeout.
|
||||
captured = nil
|
||||
if err := consumePayloadHTTPHeadersAndBody(conn); err != nil {
|
||||
if isTimeoutErr(err) {
|
||||
return last, conn, nil
|
||||
}
|
||||
return last, conn, fmt.Errorf("payload response consume failed: %w", err)
|
||||
}
|
||||
|
||||
// Keep peeking for another immediate HTTP status block. Some payload/proxy
|
||||
// chains return several statuses (for example 403 -> 403 -> 101). Returning
|
||||
// after the first status can make SSH read HTTP text instead of SSH-2.0.
|
||||
}
|
||||
|
||||
if last.StatusCode >= 400 && !p.Payload.AcceptAnyStatus && !sawSuccess {
|
||||
return last, conn, fmt.Errorf("payload rejected with final status %d", last.StatusCode)
|
||||
}
|
||||
return last, conn, nil
|
||||
}
|
||||
|
||||
func setPayloadReadDeadline(conn net.Conn, p config.Profile, attempt int) {
|
||||
timeoutMs := payloadHTTPStatusPeekTimeoutMs
|
||||
if attempt == 0 && p.Payload.ResponseTimeoutMs > 0 {
|
||||
timeoutMs = p.Payload.ResponseTimeoutMs
|
||||
if timeoutMs < payloadHTTPStatusPeekTimeoutMs {
|
||||
timeoutMs = payloadHTTPStatusPeekTimeoutMs
|
||||
}
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Duration(timeoutMs) * time.Millisecond))
|
||||
}
|
||||
|
||||
func readPayloadLinePreserveBytes(conn net.Conn, captured *bytes.Buffer, limit int) (string, error) {
|
||||
var line bytes.Buffer
|
||||
buf := make([]byte, 1)
|
||||
for line.Len() < limit {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
b := buf[0]
|
||||
_ = captured.WriteByte(b)
|
||||
_ = line.WriteByte(b)
|
||||
if b == '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if line.Len() > 0 && err == io.EOF {
|
||||
return line.String(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if line.Len() == 0 {
|
||||
return "", io.EOF
|
||||
}
|
||||
return line.String(), nil
|
||||
}
|
||||
|
||||
func consumePayloadHTTPHeadersAndBody(conn net.Conn) error {
|
||||
info := httpResponseInfo{contentLength: -1}
|
||||
for i := 0; i < maxPayloadHTTPHeaderLines; i++ {
|
||||
ignored := &bytes.Buffer{}
|
||||
line, err := readPayloadLinePreserveBytes(conn, ignored, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clean := strings.TrimSpace(line)
|
||||
if clean == "" {
|
||||
break
|
||||
}
|
||||
lower := strings.ToLower(clean)
|
||||
if strings.HasPrefix(lower, "content-length:") {
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(clean[strings.Index(clean, ":")+1:]), 10, 64); err == nil {
|
||||
info.contentLength = n
|
||||
}
|
||||
} else if strings.HasPrefix(lower, "transfer-encoding:") && strings.Contains(lower, "chunked") {
|
||||
info.chunked = true
|
||||
}
|
||||
}
|
||||
if info.chunked {
|
||||
return discardPayloadChunkedBody(conn)
|
||||
}
|
||||
if info.contentLength > 0 {
|
||||
return discardPayloadFixedLengthBody(conn, info.contentLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discardPayloadFixedLengthBody(conn net.Conn, contentLength int64) error {
|
||||
remaining := contentLength
|
||||
if remaining > maxPayloadHTTPBodyDiscardBytes {
|
||||
remaining = maxPayloadHTTPBodyDiscardBytes
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
for remaining > 0 {
|
||||
toRead := int64(len(buf))
|
||||
if remaining < toRead {
|
||||
toRead = remaining
|
||||
}
|
||||
n, err := conn.Read(buf[:int(toRead)])
|
||||
if n > 0 {
|
||||
remaining -= int64(n)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discardPayloadChunkedBody(conn net.Conn) error {
|
||||
for i := 0; i < maxPayloadHTTPHeaderLines; i++ {
|
||||
ignored := &bytes.Buffer{}
|
||||
sizeLine, err := readPayloadLinePreserveBytes(conn, ignored, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanSize := strings.TrimSpace(sizeLine)
|
||||
if semi := strings.Index(cleanSize, ";"); semi >= 0 {
|
||||
cleanSize = strings.TrimSpace(cleanSize[:semi])
|
||||
}
|
||||
chunkSize, err := strconv.ParseInt(cleanSize, 16, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if chunkSize == 0 {
|
||||
return consumePayloadTrailingHeaders(conn)
|
||||
}
|
||||
if err := discardPayloadFixedLengthBody(conn, chunkSize); err != nil {
|
||||
return err
|
||||
}
|
||||
crlf := &bytes.Buffer{}
|
||||
_, _ = readPayloadLinePreserveBytes(conn, crlf, payloadHTTPStatusLineLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func consumePayloadTrailingHeaders(conn net.Conn) error {
|
||||
for i := 0; i < maxPayloadHTTPHeaderLines; i++ {
|
||||
ignored := &bytes.Buffer{}
|
||||
line, err := readPayloadLinePreserveBytes(conn, ignored, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(line) == "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isTimeoutErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isHTTPStatusLine(statusLine string) bool {
|
||||
clean := strings.ToUpper(strings.TrimSpace(statusLine))
|
||||
return strings.HasPrefix(clean, "HTTP/1.") || strings.HasPrefix(clean, "HTTP/2") || strings.HasPrefix(clean, "HTTP/3")
|
||||
}
|
||||
|
||||
func logProxyStatus(logger *Logger, source string, responseCode int, statusLine string) {
|
||||
cleanLine := strings.TrimSpace(statusLine)
|
||||
if cleanLine == "" {
|
||||
return
|
||||
}
|
||||
if source == "" {
|
||||
source = "PROXY"
|
||||
}
|
||||
logger.Add("info", "Proxy Status [%s]: %s", source, cleanLine)
|
||||
}
|
||||
|
||||
func logHTTPCompatibilityStatus(logger *Logger, responseCode int, firstLine string) {
|
||||
switch responseCode {
|
||||
case 200:
|
||||
logger.Add("info", "Status: 200 (Connection established) Successful")
|
||||
case 101:
|
||||
logger.Add("info", "replace 200 OK")
|
||||
logger.Add("info", "HTTP/1.1 101 Websocket")
|
||||
case 100:
|
||||
logger.Add("info", "HTTP/1.1 100 Continue")
|
||||
case 301, 302, 400, 401, 403, 404, 407, 429, 500, 502, 503, 504:
|
||||
if strings.TrimSpace(firstLine) != "" {
|
||||
logger.Add("info", "%s", strings.TrimSpace(firstLine))
|
||||
} else {
|
||||
logger.Add("info", "HTTP/1.1 %d", responseCode)
|
||||
}
|
||||
logger.Add("info", "replace 200 OK")
|
||||
logger.Add("info", "Dragon Try!")
|
||||
}
|
||||
}
|
||||
|
||||
func payloadSourceLabel(mode config.Mode) string {
|
||||
switch mode {
|
||||
case config.ModePayload:
|
||||
return "HTTP_PROXY"
|
||||
case config.ModePayloadSSL:
|
||||
return "SSL_PAYLOAD"
|
||||
default:
|
||||
return "PROXY"
|
||||
}
|
||||
}
|
||||
|
||||
func buildPayload(tpl, host string, port int) string {
|
||||
portStr := strconv.Itoa(port)
|
||||
repl := map[string]string{
|
||||
"[host]": host,
|
||||
"[port]": portStr,
|
||||
"[host_port]": net.JoinHostPort(host, portStr),
|
||||
"[crlf]": "\r\n",
|
||||
"[cr]": "\r",
|
||||
"[lf]": "\n",
|
||||
"[protocol]": "HTTP/1.1",
|
||||
"[method]": "CONNECT",
|
||||
}
|
||||
out := tpl
|
||||
for k, v := range repl {
|
||||
out = strings.ReplaceAll(out, k, v)
|
||||
}
|
||||
out = replaceRotate(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func replaceRotate(s string) string {
|
||||
for {
|
||||
start := strings.Index(s, "[rotate=")
|
||||
if start < 0 {
|
||||
return s
|
||||
}
|
||||
end := strings.Index(s[start:], "]")
|
||||
if end < 0 {
|
||||
return s
|
||||
}
|
||||
end += start
|
||||
body := strings.TrimPrefix(s[start:end+1], "[rotate=")
|
||||
body = strings.TrimSuffix(body, "]")
|
||||
choices := splitRotateChoices(body)
|
||||
choice := ""
|
||||
if len(choices) > 0 {
|
||||
choice = strings.TrimSpace(choices[rand.Intn(len(choices))])
|
||||
}
|
||||
s = s[:start] + choice + s[end+1:]
|
||||
}
|
||||
}
|
||||
|
||||
func splitRotateChoices(body string) []string {
|
||||
return strings.FieldsFunc(body, func(r rune) bool {
|
||||
switch r {
|
||||
case ';', '#', ',', '\n', '\r', '\t':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func splitPayload(s string) ([]string, bool) {
|
||||
instant := strings.Contains(s, "[instant_split]")
|
||||
s = strings.ReplaceAll(s, "[instant_split]", "[split]")
|
||||
parts := strings.Split(s, "[split]")
|
||||
return parts, instant
|
||||
}
|
||||
|
||||
func parseStatusCode(status string) int {
|
||||
fields := strings.Fields(status)
|
||||
if len(fields) < 2 {
|
||||
return 0
|
||||
}
|
||||
code, _ := strconv.Atoi(fields[1])
|
||||
return code
|
||||
}
|
||||
Reference in New Issue
Block a user