400 lines
10 KiB
Go
400 lines
10 KiB
Go
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
|
|
}
|