mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
First commit, (Socks5 server!)
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
Python Edition
|
||||||
|
config.toml
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"masterhttprelayvpn/internal/client"
|
||||||
|
"masterhttprelayvpn/internal/config"
|
||||||
|
lg "masterhttprelayvpn/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := lg.New("MasterHttpRelayVPN Client", "INFO")
|
||||||
|
|
||||||
|
cfg, err := config.Load("config.toml")
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("<red>load config: <cyan>%v</cyan></red>", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := client.New(cfg, logger)
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := app.Run(ctx); err != nil {
|
||||||
|
logger.Fatalf("<red>run client: <cyan>%v</cyan></red>", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module masterhttprelayvpn
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.42.0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"masterhttprelayvpn/internal/config"
|
||||||
|
"masterhttprelayvpn/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.Config
|
||||||
|
log *logger.Logger
|
||||||
|
sessions *SessionStore
|
||||||
|
|
||||||
|
connMu sync.Mutex
|
||||||
|
conns map[net.Conn]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Config, lg *logger.Logger) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
log: lg,
|
||||||
|
sessions: NewSessionStore(),
|
||||||
|
conns: make(map[net.Conn]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Run(ctx context.Context) error {
|
||||||
|
addr := fmt.Sprintf("%s:%d", c.cfg.SOCKSHost, c.cfg.SOCKSPort)
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
c.log.Infof("<green>SOCKS5 listener started on <cyan>%s</cyan></green>", addr)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.log.Infof("<yellow>shutdown requested, closing listener and active sessions</yellow>")
|
||||||
|
_ = ln.Close()
|
||||||
|
c.closeAllConns()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
c.handleConn(ctx, conn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) registerConn(conn net.Conn) {
|
||||||
|
c.connMu.Lock()
|
||||||
|
c.conns[conn] = struct{}{}
|
||||||
|
c.connMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) unregisterConn(conn net.Conn) {
|
||||||
|
c.connMu.Lock()
|
||||||
|
delete(c.conns, conn)
|
||||||
|
c.connMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) closeAllConns() {
|
||||||
|
c.connMu.Lock()
|
||||||
|
conns := make([]net.Conn, 0, len(c.conns))
|
||||||
|
for conn := range c.conns {
|
||||||
|
conns = append(conns, conn)
|
||||||
|
}
|
||||||
|
c.connMu.Unlock()
|
||||||
|
|
||||||
|
for _, conn := range conns {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID uint64
|
||||||
|
CreatedAt time.Time
|
||||||
|
LastActivityAt time.Time
|
||||||
|
ClientAddr string
|
||||||
|
TargetHost string
|
||||||
|
TargetPort uint16
|
||||||
|
AddressType byte
|
||||||
|
InitialPayload []byte
|
||||||
|
BytesCaptured int
|
||||||
|
AuthMethod byte
|
||||||
|
UsernameUsed string
|
||||||
|
HandshakeDone bool
|
||||||
|
ConnectAccepted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) InitialPayloadHex() string {
|
||||||
|
if len(s.InitialPayload) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(s.InitialPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionStore struct {
|
||||||
|
nextID atomic.Uint64
|
||||||
|
mu sync.RWMutex
|
||||||
|
items map[uint64]*Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionStore() *SessionStore {
|
||||||
|
return &SessionStore{
|
||||||
|
items: make(map[uint64]*Session),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) New(clientAddr string) *Session {
|
||||||
|
id := s.nextID.Add(1)
|
||||||
|
now := time.Now()
|
||||||
|
session := &Session{
|
||||||
|
ID: id,
|
||||||
|
CreatedAt: now,
|
||||||
|
LastActivityAt: now,
|
||||||
|
ClientAddr: clientAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.items[id] = session
|
||||||
|
s.mu.Unlock()
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Delete(id uint64) {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.items, id)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
socksVersion5 = 0x05
|
||||||
|
|
||||||
|
socksMethodNoAuth = 0x00
|
||||||
|
socksMethodUserPass = 0x02
|
||||||
|
socksMethodNoAcceptable = 0xFF
|
||||||
|
|
||||||
|
socksCmdConnect = 0x01
|
||||||
|
|
||||||
|
socksAtypIPv4 = 0x01
|
||||||
|
socksAtypDomain = 0x03
|
||||||
|
socksAtypIPv6 = 0x04
|
||||||
|
|
||||||
|
socksReplySuccess = 0x00
|
||||||
|
socksReplyGeneralFailure = 0x01
|
||||||
|
socksReplyCommandUnsupported = 0x07
|
||||||
|
socksReplyAddressUnsupported = 0x08
|
||||||
|
|
||||||
|
socksUserPassVersion = 0x01
|
||||||
|
socksAuthSuccess = 0x00
|
||||||
|
socksAuthFailure = 0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) handleConn(ctx context.Context, conn net.Conn) {
|
||||||
|
c.registerConn(conn)
|
||||||
|
defer c.unregisterConn(conn)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
session := c.sessions.New(conn.RemoteAddr().String())
|
||||||
|
defer c.sessions.Delete(session.ID)
|
||||||
|
|
||||||
|
c.log.Infof("<green>accepted client <cyan>%s</cyan> session=<cyan>%d</cyan></green>", conn.RemoteAddr(), session.ID)
|
||||||
|
|
||||||
|
if err := c.handleSOCKS5(ctx, conn, session); err != nil {
|
||||||
|
c.log.Errorf("<red>session=<cyan>%d</cyan> closed: <cyan>%v</cyan></red>", session.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleSOCKS5(ctx context.Context, conn net.Conn, session *Session) error {
|
||||||
|
version := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(conn, version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if version[0] != socksVersion5 {
|
||||||
|
return fmt.Errorf("<red>unsupported SOCKS version: <cyan>%d</cyan></red>", version[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
method, err := c.negotiateAuth(conn, session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == socksMethodUserPass {
|
||||||
|
if err := c.handleUserPassAuth(conn, session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetHost, targetPort, atyp, err := readConnectRequest(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session.TargetHost = targetHost
|
||||||
|
session.TargetPort = targetPort
|
||||||
|
session.AddressType = atyp
|
||||||
|
session.ConnectAccepted = true
|
||||||
|
session.HandshakeDone = true
|
||||||
|
session.LastActivityAt = time.Now()
|
||||||
|
|
||||||
|
if err := writeSocksReply(conn, socksReplySuccess); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log.Infof(
|
||||||
|
"<green>session=<cyan>%d</cyan> CONNECT target=<cyan>%s:%d</cyan> auth_method=<cyan>%d</cyan></green>",
|
||||||
|
session.ID, session.TargetHost, session.TargetPort, session.AuthMethod,
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.captureInitialPayload(ctx, conn, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) negotiateAuth(conn net.Conn, session *Session) (byte, error) {
|
||||||
|
countBuf := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(conn, countBuf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
methodCount := int(countBuf[0])
|
||||||
|
methods := make([]byte, methodCount)
|
||||||
|
if _, err := io.ReadFull(conn, methods); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := byte(socksMethodNoAcceptable)
|
||||||
|
if c.cfg.SOCKSAuth {
|
||||||
|
if slices.Contains(methods, socksMethodUserPass) {
|
||||||
|
selected = socksMethodUserPass
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if slices.Contains(methods, socksMethodNoAuth) {
|
||||||
|
selected = socksMethodNoAuth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Write([]byte{socksVersion5, selected}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if selected == socksMethodNoAcceptable {
|
||||||
|
return 0, errors.New("no acceptable auth method")
|
||||||
|
}
|
||||||
|
|
||||||
|
session.AuthMethod = selected
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleUserPassAuth(conn net.Conn, session *Session) error {
|
||||||
|
header := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if header[0] != socksUserPassVersion {
|
||||||
|
return fmt.Errorf("<red>invalid username/password auth version: <cyan>%d</cyan></red>", header[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
username := make([]byte, int(header[1]))
|
||||||
|
if _, err := io.ReadFull(conn, username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
passLen := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(conn, passLen); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
password := make([]byte, int(passLen[0]))
|
||||||
|
if _, err := io.ReadFull(conn, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := string(username) == c.cfg.SOCKSUsername && string(password) == c.cfg.SOCKSPassword
|
||||||
|
session.UsernameUsed = string(username)
|
||||||
|
if ok {
|
||||||
|
_, err := conn.Write([]byte{socksUserPassVersion, socksAuthSuccess})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = conn.Write([]byte{socksUserPassVersion, socksAuthFailure})
|
||||||
|
return errors.New("invalid SOCKS username/password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConnectRequest(conn net.Conn) (string, uint16, byte, error) {
|
||||||
|
header := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, header); err != nil {
|
||||||
|
return "", 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if header[0] != socksVersion5 {
|
||||||
|
return "", 0, 0, fmt.Errorf("<red>invalid request version: <cyan>%d</cyan></red>", header[0])
|
||||||
|
}
|
||||||
|
if header[1] != socksCmdConnect {
|
||||||
|
_ = writeSocksReply(conn, socksReplyCommandUnsupported)
|
||||||
|
return "", 0, 0, fmt.Errorf("<red>unsupported SOCKS command: <cyan>%d</cyan></red>", header[1])
|
||||||
|
}
|
||||||
|
if header[2] != 0x00 {
|
||||||
|
return "", 0, 0, errors.New("non-zero reserved byte in SOCKS request")
|
||||||
|
}
|
||||||
|
|
||||||
|
atyp := header[3]
|
||||||
|
host, err := readTargetHost(conn, atyp)
|
||||||
|
if err != nil {
|
||||||
|
_ = writeSocksReply(conn, socksReplyAddressUnsupported)
|
||||||
|
return "", 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
portBytes := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, portBytes); err != nil {
|
||||||
|
return "", 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return host, binary.BigEndian.Uint16(portBytes), atyp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTargetHost(conn net.Conn, atyp byte) (string, error) {
|
||||||
|
switch atyp {
|
||||||
|
case socksAtypIPv4:
|
||||||
|
ip := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, ip); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return net.IP(ip).String(), nil
|
||||||
|
case socksAtypIPv6:
|
||||||
|
ip := make([]byte, 16)
|
||||||
|
if _, err := io.ReadFull(conn, ip); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return net.IP(ip).String(), nil
|
||||||
|
case socksAtypDomain:
|
||||||
|
size := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(conn, size); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
domain := make([]byte, int(size[0]))
|
||||||
|
if _, err := io.ReadFull(conn, domain); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(domain), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("<red>unsupported address type: <cyan>%d</cyan></red>", atyp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSocksReply(conn net.Conn, reply byte) error {
|
||||||
|
resp := []byte{
|
||||||
|
socksVersion5,
|
||||||
|
reply,
|
||||||
|
0x00,
|
||||||
|
socksAtypIPv4,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
}
|
||||||
|
_, err := conn.Write(resp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) captureInitialPayload(ctx context.Context, conn net.Conn, session *Session) error {
|
||||||
|
peekTimeout := 2 * time.Second
|
||||||
|
idleTimeout := 30 * time.Second
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(peekTimeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err == nil && n > 0 {
|
||||||
|
session.InitialPayload = append([]byte(nil), buf[:n]...)
|
||||||
|
session.BytesCaptured += n
|
||||||
|
session.LastActivityAt = time.Now()
|
||||||
|
c.log.Infof(
|
||||||
|
"<green>session=<cyan>%d</cyan> captured initial payload bytes=<cyan>%d</cyan> target=<cyan>%s</cyan></green>",
|
||||||
|
session.ID, n, net.JoinHostPort(session.TargetHost, strconv.Itoa(int(session.TargetPort))),
|
||||||
|
)
|
||||||
|
} else if ne, ok := err.(net.Error); !ok || !ne.Timeout() {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(idleTimeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
session.BytesCaptured += n
|
||||||
|
session.LastActivityAt = time.Now()
|
||||||
|
c.log.Debugf("<green>session=<cyan>%d</cyan> buffered payload chunk=<cyan>%d</cyan> total=<cyan>%d</cyan></green>", session.ID, n, session.BytesCaptured)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AESEncryptionKey string
|
||||||
|
SOCKSHost string
|
||||||
|
SOCKSPort int
|
||||||
|
SOCKSAuth bool
|
||||||
|
SOCKSUsername string
|
||||||
|
SOCKSPassword string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
cfg := Config{
|
||||||
|
SOCKSHost: "127.0.0.1",
|
||||||
|
SOCKSPort: 1080,
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key, value, ok := strings.Cut(line, "=")
|
||||||
|
if !ok {
|
||||||
|
return Config{}, fmt.Errorf("invalid config line: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "AES_ENCRYPTION_KEY":
|
||||||
|
cfg.AESEncryptionKey = trimString(value)
|
||||||
|
case "SOCKS_HOST":
|
||||||
|
cfg.SOCKSHost = trimString(value)
|
||||||
|
case "SOCKS_PORT":
|
||||||
|
port, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse SOCKS_PORT: %w", err)
|
||||||
|
}
|
||||||
|
cfg.SOCKSPort = port
|
||||||
|
case "SOCKS_AUTH":
|
||||||
|
auth, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse SOCKS_AUTH: %w", err)
|
||||||
|
}
|
||||||
|
cfg.SOCKSAuth = auth
|
||||||
|
case "SOCKS_USERNAME":
|
||||||
|
cfg.SOCKSUsername = trimString(value)
|
||||||
|
case "SOCKS_PASSWORD":
|
||||||
|
cfg.SOCKSPassword = trimString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SOCKSAuth && (cfg.SOCKSUsername == "" || cfg.SOCKSPassword == "") {
|
||||||
|
return Config{}, fmt.Errorf("SOCKS auth enabled but username/password missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SOCKSPort < 1 || cfg.SOCKSPort > 65535 {
|
||||||
|
return Config{}, fmt.Errorf("invalid SOCKS_PORT: %d", cfg.SOCKSPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimString(value string) string {
|
||||||
|
return strings.Trim(value, `"`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func detectColorSupport(w io.Writer) bool {
|
||||||
|
f, ok := w.(*os.File)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (info.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const enableVirtualTerminalProcessing = 0x0004
|
||||||
|
|
||||||
|
func detectColorSupport(w io.Writer) bool {
|
||||||
|
f, ok := w.(*os.File)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := windows.Handle(f.Fd())
|
||||||
|
var mode uint32
|
||||||
|
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode&enableVirtualTerminalProcessing != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return windows.SetConsoleMode(handle, mode|enableVirtualTerminalProcessing) == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
name string
|
||||||
|
level int
|
||||||
|
mu sync.Mutex
|
||||||
|
consoleWriter io.Writer
|
||||||
|
fileWriter *os.File
|
||||||
|
color bool
|
||||||
|
appNameText string
|
||||||
|
appNameColored string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
levelDebug = iota
|
||||||
|
levelInfo
|
||||||
|
levelWarn
|
||||||
|
levelError
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelDebug = levelDebug
|
||||||
|
LevelInfo = levelInfo
|
||||||
|
LevelWarn = levelWarn
|
||||||
|
LevelError = levelError
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorTagCodes = map[string]string{
|
||||||
|
"black": "\x1b[30m",
|
||||||
|
"red": "\x1b[31m",
|
||||||
|
"green": "\x1b[32m",
|
||||||
|
"yellow": "\x1b[33m",
|
||||||
|
"blue": "\x1b[34m",
|
||||||
|
"magenta": "\x1b[35m",
|
||||||
|
"cyan": "\x1b[36m",
|
||||||
|
"white": "\x1b[37m",
|
||||||
|
"gray": "\x1b[90m",
|
||||||
|
"grey": "\x1b[90m",
|
||||||
|
"bold": "\x1b[1m",
|
||||||
|
"reset": "\x1b[0m",
|
||||||
|
}
|
||||||
|
|
||||||
|
var plainLevelTexts = [...]string{
|
||||||
|
levelDebug: "[DEBUG]",
|
||||||
|
levelInfo: "[INFO]",
|
||||||
|
levelWarn: "[WARN]",
|
||||||
|
levelError: "[ERROR]",
|
||||||
|
}
|
||||||
|
|
||||||
|
var coloredLevelTexts = [...]string{
|
||||||
|
levelDebug: "\x1b[35m[DEBUG]\x1b[0m",
|
||||||
|
levelInfo: "\x1b[32m[INFO]\x1b[0m",
|
||||||
|
levelWarn: "\x1b[33m[WARN]\x1b[0m",
|
||||||
|
levelError: "\x1b[31m[ERROR]\x1b[0m",
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(name, rawLevel string) *Logger {
|
||||||
|
return NewWithFile(name, rawLevel, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithFile(name, rawLevel, filePath string) *Logger {
|
||||||
|
appName := "[" + name + "]"
|
||||||
|
var consoleWriter io.Writer = os.Stdout
|
||||||
|
var fileWriter *os.File
|
||||||
|
|
||||||
|
if filePath != "" {
|
||||||
|
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err == nil {
|
||||||
|
fileWriter = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Logger{
|
||||||
|
name: name,
|
||||||
|
level: parseLevel(rawLevel),
|
||||||
|
consoleWriter: consoleWriter,
|
||||||
|
fileWriter: fileWriter,
|
||||||
|
color: shouldUseColor(),
|
||||||
|
appNameText: appName,
|
||||||
|
appNameColored: "\x1b[36m" + appName + "\x1b[0m",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLevel(raw string) int {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(raw)) {
|
||||||
|
case "DEBUG":
|
||||||
|
return levelDebug
|
||||||
|
case "WARNING", "WARN":
|
||||||
|
return levelWarn
|
||||||
|
case "ERROR", "CRITICAL":
|
||||||
|
return levelError
|
||||||
|
default:
|
||||||
|
return levelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logf(level int, format string, args ...any) {
|
||||||
|
if l == nil || level < l.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := format
|
||||||
|
if len(args) != 0 {
|
||||||
|
msg = fmt.Sprintf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
plainMsg := msg
|
||||||
|
if strings.IndexByte(msg, '<') >= 0 {
|
||||||
|
plainMsg = stripColorTags(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
ts := time.Now().Format("2006/01/02 15:04:05")
|
||||||
|
|
||||||
|
if l.fileWriter != nil {
|
||||||
|
fileLine := ts + " " + plainLevelTexts[level] + " " + plainMsg + "\n"
|
||||||
|
if _, err := io.WriteString(l.fileWriter, fileLine); err != nil {
|
||||||
|
_ = l.fileWriter.Close()
|
||||||
|
l.fileWriter = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.consoleWriter != nil {
|
||||||
|
appName := l.appNameText
|
||||||
|
levelText := plainLevelTexts[level]
|
||||||
|
finalMsg := plainMsg
|
||||||
|
|
||||||
|
if l.color {
|
||||||
|
if strings.IndexByte(msg, '<') >= 0 {
|
||||||
|
finalMsg = renderColorTags(msg)
|
||||||
|
} else {
|
||||||
|
finalMsg = msg
|
||||||
|
}
|
||||||
|
appName = l.appNameColored
|
||||||
|
levelText = coloredLevelTexts[level]
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleLine := ts + " " + appName + " " + levelText + " " + finalMsg + "\n"
|
||||||
|
_, _ = io.WriteString(l.consoleWriter, consoleLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Debugf(format string, args ...any) { l.logf(levelDebug, format, args...) }
|
||||||
|
func (l *Logger) Infof(format string, args ...any) { l.logf(levelInfo, format, args...) }
|
||||||
|
func (l *Logger) Warnf(format string, args ...any) { l.logf(levelWarn, format, args...) }
|
||||||
|
func (l *Logger) Errorf(format string, args ...any) { l.logf(levelError, format, args...) }
|
||||||
|
|
||||||
|
func (l *Logger) Fatalf(format string, args ...any) {
|
||||||
|
l.logf(levelError, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Enabled(level int) bool {
|
||||||
|
return l != nil && level >= l.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripColorTags(text string) string {
|
||||||
|
start := strings.IndexByte(text, '<')
|
||||||
|
if start == -1 {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(text))
|
||||||
|
|
||||||
|
for i := 0; i < len(text); {
|
||||||
|
if text[i] != '<' {
|
||||||
|
next := strings.IndexByte(text[i:], '<')
|
||||||
|
if next == -1 {
|
||||||
|
b.WriteString(text[i:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(text[i : i+next])
|
||||||
|
i += next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
end := strings.IndexByte(text[i:], '>')
|
||||||
|
if end == -1 {
|
||||||
|
b.WriteString(text[i:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTag := text[i : i+end+1]
|
||||||
|
tag := strings.ToLower(rawTag)
|
||||||
|
if _, _, ok := parseColorTag(tag); ok {
|
||||||
|
i += end + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(rawTag)
|
||||||
|
i += end + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUseColor() bool {
|
||||||
|
if strings.TrimSpace(os.Getenv("NO_COLOR")) != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(os.Getenv("FORCE_COLOR")) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return detectColorSupport(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderColorTags(text string) string {
|
||||||
|
start := strings.IndexByte(text, '<')
|
||||||
|
if start == -1 {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(text) + 16)
|
||||||
|
b.WriteString(text[:start])
|
||||||
|
stack := make([]string, 0, 4)
|
||||||
|
|
||||||
|
for i := start; i < len(text); {
|
||||||
|
if text[i] != '<' {
|
||||||
|
next := strings.IndexByte(text[i:], '<')
|
||||||
|
if next == -1 {
|
||||||
|
b.WriteString(text[i:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(text[i : i+next])
|
||||||
|
i += next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
end := strings.IndexByte(text[i:], '>')
|
||||||
|
if end == -1 {
|
||||||
|
b.WriteString(text[i:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTag := text[i : i+end+1]
|
||||||
|
tag := strings.ToLower(rawTag)
|
||||||
|
if name, closing, ok := parseColorTag(tag); ok {
|
||||||
|
if closing {
|
||||||
|
if name == "reset" {
|
||||||
|
stack = stack[:0]
|
||||||
|
b.WriteString("\x1b[0m")
|
||||||
|
} else if restoreColorTag(&stack, name) {
|
||||||
|
b.WriteString("\x1b[0m")
|
||||||
|
for _, active := range stack {
|
||||||
|
b.WriteString(colorTagCodes[active])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteString(rawTag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stack = append(stack, name)
|
||||||
|
b.WriteString(colorTagCodes[name])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteString(rawTag)
|
||||||
|
}
|
||||||
|
i += end + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stack) != 0 {
|
||||||
|
b.WriteString("\x1b[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseColorTag(tag string) (name string, closing bool, ok bool) {
|
||||||
|
if len(tag) < 3 || tag[0] != '<' || tag[len(tag)-1] != '>' {
|
||||||
|
return "", false, false
|
||||||
|
}
|
||||||
|
closing = strings.HasPrefix(tag, "</")
|
||||||
|
if closing {
|
||||||
|
name = tag[2 : len(tag)-1]
|
||||||
|
} else {
|
||||||
|
name = tag[1 : len(tag)-1]
|
||||||
|
}
|
||||||
|
_, ok = colorTagCodes[name]
|
||||||
|
return name, closing, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreColorTag(stack *[]string, name string) bool {
|
||||||
|
if stack == nil || len(*stack) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
items := *stack
|
||||||
|
for idx := len(items) - 1; idx >= 0; idx-- {
|
||||||
|
if items[idx] != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
copy(items[idx:], items[idx+1:])
|
||||||
|
lastIdx := len(items) - 1
|
||||||
|
items[lastIdx] = ""
|
||||||
|
*stack = items[:lastIdx]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NowUnixNano() int64 {
|
||||||
|
return time.Now().UnixNano()
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// ==============================================================================
|
||||||
|
// MasterHttpRelayVPN
|
||||||
|
// Author: MasterkinG32
|
||||||
|
// Github: https://github.com/masterking32
|
||||||
|
// Year: 2026
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseLevel(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
raw string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{raw: "debug", want: levelDebug},
|
||||||
|
{raw: "INFO", want: levelInfo},
|
||||||
|
{raw: "warn", want: levelWarn},
|
||||||
|
{raw: "warning", want: levelWarn},
|
||||||
|
{raw: "critical", want: levelError},
|
||||||
|
{raw: "error", want: levelError},
|
||||||
|
{raw: "unknown", want: levelInfo},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := parseLevel(tt.raw); got != tt.want {
|
||||||
|
t.Fatalf("parseLevel(%q) = %d, want %d", tt.raw, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderColorTags(t *testing.T) {
|
||||||
|
got := renderColorTags("<green>ok</green> <cyan>test</cyan> <unknown>x</unknown>")
|
||||||
|
if !strings.Contains(got, "\x1b[32m") {
|
||||||
|
t.Fatal("expected green ANSI code in rendered string")
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "\x1b[36m") {
|
||||||
|
t.Fatal("expected cyan ANSI code in rendered string")
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "<unknown>x</unknown>") {
|
||||||
|
t.Fatal("unknown tags should be preserved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderColorTagsRestoresParentColor(t *testing.T) {
|
||||||
|
got := renderColorTags("<green>Listener <cyan>127.0.0.1:5350</cyan> Ready</green>")
|
||||||
|
want := "\x1b[32mListener \x1b[36m127.0.0.1:5350\x1b[0m\x1b[32m Ready\x1b[0m"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("renderColorTags() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerSuppressesBelowLevel(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
l := &Logger{
|
||||||
|
name: "test",
|
||||||
|
level: levelWarn,
|
||||||
|
consoleWriter: &buf,
|
||||||
|
color: false,
|
||||||
|
appNameText: "[test]",
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infof("info message")
|
||||||
|
l.Warnf("warn message")
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if strings.Contains(output, "info message") {
|
||||||
|
t.Fatal("info message should be suppressed at WARN level")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "warn message") {
|
||||||
|
t.Fatal("warn message should be logged at WARN level")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldUseColorHonorsNoColor(t *testing.T) {
|
||||||
|
oldNoColor := os.Getenv("NO_COLOR")
|
||||||
|
oldForceColor := os.Getenv("FORCE_COLOR")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Setenv("NO_COLOR", oldNoColor)
|
||||||
|
_ = os.Setenv("FORCE_COLOR", oldForceColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = os.Setenv("FORCE_COLOR", "1")
|
||||||
|
_ = os.Setenv("NO_COLOR", "1")
|
||||||
|
|
||||||
|
if shouldUseColor() {
|
||||||
|
t.Fatal("NO_COLOR should disable colors even when FORCE_COLOR is set")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user