working commit

This commit is contained in:
2026-05-26 17:11:13 +02:00
commit 2e59f88d76
103 changed files with 18276 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package config
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"mproxy/pkg/auxx509"
yaml "go.yaml.in/yaml/v4"
)
type Service struct {
Address string `json:"address" yaml:"address"`
Port uint32 `json:"port" yaml:"port"`
}
type Database struct {
Basepath string `json:"basepath" yaml:"basepath"`
}
type Storage struct {
Basepath string `json:"basepath" yaml:"basepath"`
}
type Config struct {
Service Service `json:"service" yaml:"service"`
Database Database `json:"database" yaml:"database"`
Storage Storage `json:"storage" yaml:"storage"`
AsDaemon bool `json:"asDaemon" yaml:"asDaemon"`
Logpath string `json:"logpath" yaml:"logpath"`
Runpath string `json:"runpath" yaml:"runpath"`
Version string `json:"version" yaml:"version"`
Certpath string `json:"certpath,omitempty" yaml:"certpath,omitempty"`
Keypath string `json:"keypath,omitempty" yaml:"keypath,omitempty"`
X509Cert string `json:"-" yaml:"-"`
X509Key string `json:"-" yaml:"-"`
Datadir string `json:"datadir" yaml:"datadir"`
Hostname string `json:"hostname" yaml:"hostname"`
Hostnames []string `json:"hostnames" yaml:"hostnames"`
LogLimit int64 `json:"logLimit" yaml:"logLimit"`
RunUser string `json:"runUser" yaml:"runUser"`
}
func NewConfig() *Config {
logfile := fmt.Sprintf("%s.log", srvname)
logpath := filepath.Join(logdir, logfile)
runfile := fmt.Sprintf("%s.pid", srvname)
runpath := filepath.Join(rundir, runfile)
//certpath := fmt.Sprintf("%s.crt", srvname)
//certpath = filepath.Join(confdir, certpath)
//keypath := fmt.Sprintf("%s.crt", srvname)
//keypath = filepath.Join(confdir, keypath)
return &Config{
Service: Service{
Address: "0.0.0.0",
Port: 1025,
},
Database: Database{
Basepath: datadir,
},
Storage: Storage{
Basepath: datadir,
},
AsDaemon: false,
Logpath: logpath,
Runpath: runpath,
Version: version,
Datadir: datadir,
//Certpath: certpath,
//Keypath: keypath,
Hostnames: make([]string, 0),
LogLimit: 1024 * 1024 * 10, // 10 Mb
RunUser: "daemon",
}
}
func (conf *Config) String() string {
confbytes, _ := yaml.Marshal(conf)
return string(confbytes)
}
func (conf *Config) ReadConfigfile() error {
conffile := fmt.Sprintf("%s.yaml", srvname)
confpath := filepath.Join(confdir, conffile)
confdata, err := ioutil.ReadFile(confpath)
if err != nil {
return err
}
err = yaml.Unmarshal(confdata, conf)
if err != nil {
return err
}
return err
}
func (conf *Config) ReadX509Cert() error {
var err error
if conf.Certpath != "" && conf.Keypath != "" {
if !filepath.IsAbs(conf.Certpath) {
conf.Certpath = filepath.Join(confdir, conf.Certpath)
}
certBytes, err := os.ReadFile(conf.Certpath)
if err != nil {
return err
}
if !filepath.IsAbs(conf.Keypath) {
conf.Keypath = filepath.Join(confdir, conf.Keypath)
}
keyBytes, err := os.ReadFile(conf.Keypath)
if err != nil {
return err
}
conf.X509Cert = string(certBytes)
conf.X509Key = string(keyBytes)
return err
}
/*
if conf.X509Cert != "" && conf.X509Key != "" {
x509Cert, err := base64.StdEncoding.DecodeString(conf.X509Cert)
if err != nil {
return err
}
conf.X509Cert = string(x509Cert)
x509Key, err := base64.StdEncoding.DecodeString(conf.X509Key)
if err != nil {
return err
}
conf.X509Key = string(x509Key)
}
*/
if conf.X509Cert == "" || conf.X509Key == "" {
if conf.Hostname == "" {
conf.Hostname, err = os.Hostname()
if err != nil {
return err
}
}
certBytes, keyBytes, err := auxx509.CreateSelfSignedCert(conf.Hostname, conf.Hostnames...)
if err != nil {
return err
}
conf.X509Cert = string(certBytes)
conf.X509Key = string(keyBytes)
return err
}
return err
}
+10
View File
@@ -0,0 +1,10 @@
package config
const (
confdir = "@srv_confdir@"
rundir = "@srv_rundir@"
logdir = "@srv_logdir@"
datadir = "@srv_datadir@"
version = "@PACKAGE_VERSION@"
srvname = "@PACKAGE_NAME@d"
)
+66
View File
@@ -0,0 +1,66 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package handler
import (
"context"
"fmt"
"mproxy/app/router"
"mproxy/pkg/auxhttp"
)
const (
authTag = "authpass"
userTag = "accountID"
)
func (hand *Handler) AuthMiddleware(next router.Handler) router.Handler {
var handlerFunc router.HandlerFunc
handlerFunc = func(rctx *router.Context) {
success, err := hand.CheckAccess(rctx)
if success {
rctx.SetBool(authTag, true)
}
if err != nil {
hand.logg.Errorf("Authorization middleware error: %v", err)
}
next.ServeHTTP(rctx)
}
return handlerFunc
}
// Authentification
func (hand *Handler) CheckAccess(rctx *router.Context) (bool, error) {
var err error
var success bool
var username string
var password string
authHeader := rctx.GetHeader("Proxy-Authorization")
hand.logg.Debugf("Proxy-Authorization: [%s]", authHeader)
if authHeader != "" {
username, password, err = auxhttp.ParseBasicAuth(authHeader)
if err != nil {
return success, err
}
success, err := hand.ValidatePassword(rctx.Ctx, username, password)
if err != nil {
return false, err
}
if !success {
err = fmt.Errorf("Incorrect username or password")
return false, err
}
return success, err
}
return success, err
}
func (hand *Handler) ValidatePassword(ctx context.Context, username, password string) (bool, error) {
var err error
valid := true
return valid, err
}
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package handler
import (
"net/http"
"mproxy/app/proxoper"
"mproxy/app/router"
)
func (hand *Handler) ConnectTo(rctx *router.Context) {
hostaddr, _ := rctx.GetSubpath("hostaddr")
params := &proxoper.ConnectToParams{
Hostaddr: hostaddr,
}
ctx := rctx.GetContext()
_, err := hand.prop.ConnectTo(ctx, params)
if err != nil {
hand.logg.Errorf("ConnectTo error: %v", err)
rctx.SetStatus(http.StatusInternalServerError)
return
}
rctx.SetStatus(http.StatusOK)
return
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package handler
import (
"mproxy/app/logger"
"mproxy/app/router"
"mproxy/app/proxoper"
"mproxy/app/servoper"
yaml "go.yaml.in/yaml/v4"
)
type HandlerParams struct {
ServOper *servoper.Operator
ProxOper *proxoper.Operator
}
type Handler struct {
logg *logger.Logger
seop *servoper.Operator
prop *proxoper.Operator
}
func NewHandler(params *HandlerParams) (*Handler, error) {
var err error
hand := &Handler{
seop: params.ServOper,
prop: params.ProxOper,
}
hand.logg = logger.NewLoggerWithSubject("handler")
return hand, err
}
func (hand *Handler) DumpHeaders(label string, rctx *router.Context) {
headers := rctx.GetHeaders()
yamlData, _ := yaml.Marshal(headers)
hand.logg.Debugf("%s:\n%s\n", label, string(yamlData))
}
+15
View File
@@ -0,0 +1,15 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package handler
import (
"net/http"
"mproxy/app/router"
)
func (hand *Handler) NotFound(rctx *router.Context) {
hand.logg.Warningf("Route for [%s %s] not found", rctx.Request.Method, rctx.Request.URL.String())
rctx.SetStatus(http.StatusNotFound)
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package handler
import (
"net/http"
"mproxy/app/router"
)
type Response[T any] struct {
Error bool `json:"error" yaml:"error"`
Message string `json:"message,omitempty" yaml:"message,omitempty"`
Result T `json:"result,omitempty" yaml:"result,result"`
}
func NewResponse[T any]() *Response[T] {
return &Response[T]{}
}
func (hand *Handler) SendResult(rctx *router.Context, result any) {
response := &Response[any]{
Error: false,
Result: result,
}
rctx.SendJSON(http.StatusOK, response)
}
func (hand *Handler) SendError(rctx *router.Context, err error) {
response := &Response[any]{
Error: true,
Message: err.Error(),
}
rctx.SendJSON(http.StatusOK, response)
}
+15
View File
@@ -0,0 +1,15 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package handler
import (
"mproxy/app/router"
"mproxy/app/servoper"
)
func (hand *Handler) SendHello(rctx *router.Context) {
params := &servoper.SendHelloParams{}
res, _ := hand.seop.SendHello(params)
hand.SendResult(rctx, res)
}
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package logger
import (
"bytes"
"fmt"
"io"
"os"
"sync"
"time"
)
var (
mtx sync.Mutex
output io.WriteCloser = os.Stderr
)
type Logger struct {
subject string
writer io.WriteCloser
mtx *sync.Mutex
}
func NewLoggerWithSubject(subj string) *Logger {
return &Logger{
subject: subj,
writer: output,
mtx: &mtx,
}
}
func NewLogger() *Logger {
return &Logger{
writer: output,
mtx: &mtx,
}
}
func SetWriter(newOut io.WriteCloser) {
mtx.Lock()
output = newOut
mtx.Unlock()
}
func (logg *Logger) SetWriter(newOut io.WriteCloser) {
mtx.Lock()
logg.writer = newOut
var newMtx sync.Mutex
logg.mtx = &newMtx
mtx.Unlock()
}
func (logg *Logger) Debugf(message string, args ...any) {
logg.printf("debug", message, args...)
}
func (logg *Logger) Infof(message string, args ...any) {
logg.printf("info", message, args...)
}
func (logg *Logger) Warningf(message string, args ...any) {
logg.printf("warning", message, args...)
}
func (logg *Logger) Errorf(message string, args ...any) {
logg.printf("error", message, args...)
}
func (logg *Logger) printf(level, message string, args ...any) {
timestamp := time.Now().Format(time.RFC3339)
buffer := bytes.NewBuffer([]byte{})
if logg.subject != "" {
fmt.Fprintf(buffer, "%s %s.%s: ", timestamp, logg.subject, level)
} else {
fmt.Fprintf(buffer, "%s %s: ", timestamp, level)
}
fmt.Fprintf(buffer, message, args...)
fmt.Fprintf(buffer, "\n")
logg.mtx.Lock()
fmt.Fprint(output, buffer.String())
logg.mtx.Unlock()
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package logger
import (
"io/ioutil"
"testing"
)
func TestLogger(t *testing.T) {
logg := NewLogger("test")
logg.Debugf("foo: %s", "bar")
}
func BenchmarkLoggerL(b *testing.B) {
SetWriter(ioutil.Discard)
logg := NewLogger("test")
for i := 0; i < b.N; i++ {
logg.Debugf("foo: %s", "bar")
}
}
func BenchmarkLoggerP(b *testing.B) {
SetWriter(ioutil.Discard)
logg := NewLogger("test")
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logg.Debugf("foo: %s", "bar")
}
})
}
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package proxoper
import (
"context"
"io"
)
type ConnectToParams struct {
Hostaddr string
}
type ConnectToResult struct {
Stream io.Writer
}
func (oper *Operator) ConnectTo(ctx context.Context, param *ConnectToParams) (*ConnectToResult, error) {
var err error
res := &ConnectToResult{}
return res, err
}
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package proxoper
import (
"mproxy/app/logger"
)
type OperatorParams struct{}
type Operator struct {
logg *logger.Logger
}
func NewOperator(params *OperatorParams) (*Operator, error) {
var err error
oper := &Operator{}
oper.logg = logger.NewLoggerWithSubject("proxoper")
return oper, err
}
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"reflect"
"strconv"
)
// The code reflect string-string map to taged structure
// Limited, used only base types
// Don't ask me how it works. I'm only writer ;)
func bindObj(obj interface{}, kvmap map[string]string, sTag string) error {
var err error
vElem := reflect.ValueOf(obj).Elem()
sElem := reflect.TypeOf(obj).Elem()
for i := 0; i < vElem.NumField(); i++ {
vField := vElem.Field(i)
tag := sElem.Field(i).Tag.Get(sTag)
if tag != "" {
sVal, exists := kvmap[tag]
if exists {
switch vField.Kind() {
case reflect.String:
vField.SetString(sVal)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
iVal, err := strconv.ParseInt(sVal, 10, 64)
if err != nil {
return err
}
vField.SetInt(iVal)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
iVal, err := strconv.ParseUint(sVal, 10, 64)
if err != nil {
return err
}
vField.SetUint(iVal)
case reflect.Bool:
bVal, err := strconv.ParseBool(sVal)
if err != nil {
return err
}
vField.SetBool(bVal)
case reflect.Float32, reflect.Float64:
fVal, err := strconv.ParseFloat(sVal, 64)
if err != nil {
return err
}
vField.SetFloat(fVal)
}
}
}
}
return err
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
)
type Context struct {
Ctx context.Context
Request *http.Request
Writer http.ResponseWriter
PathMap map[string]string
Bools map[string]bool
Strings map[string]string
StatusCode int
}
func NewContext(writer http.ResponseWriter, request *http.Request) *Context {
rctx := &Context{
Writer: writer,
Request: request,
Ctx: request.Context(),
PathMap: make(map[string]string),
Bools: make(map[string]bool),
Strings: make(map[string]string),
}
return rctx
}
// Aux maps
func (rctx *Context) SetBool(key string, value bool) {
rctx.Bools[key] = value
}
func (rctx *Context) GetBool(key string) (bool, bool) {
exists, value := rctx.Bools[key]
return exists, value
}
func (rctx *Context) SetString(key string, value string) {
rctx.Strings[key] = value
}
func (rctx *Context) GetString(key string) (string, bool) {
value, exists := rctx.Strings[key]
return value, exists
}
// Request
func (rctx *Context) GetSubpath(key string) (string, bool) {
value, exists := rctx.PathMap[key]
return value, exists
}
func (rctx *Context) URL() *url.URL {
return rctx.Request.URL
}
func (rctx *Context) GetQuery(key string) string {
return rctx.Request.URL.Query().Get(key)
}
func (rctx *Context) GetHeader(key string) string {
return rctx.Request.Header.Get(key)
}
func (rctx *Context) GetHeaders() http.Header {
return rctx.Request.Header
}
func (rctx *Context) GetContext() context.Context {
return rctx.Request.Context()
}
// Binding
const emptyJSON = "{}"
func (rctx *Context) BindJSON(obj any) error {
buffer := bytes.NewBuffer(nil)
_, err := io.Copy(buffer, rctx.Request.Body)
if err != nil {
return err
}
reqBody := buffer.Bytes()
if len(reqBody) == 0 {
reqBody = []byte(emptyJSON)
}
err = json.Unmarshal(reqBody, obj)
if err != nil {
return err
}
return err
}
func (rctx *Context) BindQuery(obj any) error {
qMap := make(map[string]string)
for key, val := range rctx.Request.URL.Query() {
if len(val) == 1 {
qMap[key] = val[0]
}
}
return bindObj(obj, qMap, "param")
}
// Response
func (rctx *Context) SetHeader(key, value string) {
rctx.Writer.Header().Set(key, value)
}
func (rctx *Context) SetStatus(httpStatus int) {
rctx.StatusCode = httpStatus
rctx.Writer.WriteHeader(httpStatus)
}
func (rctx *Context) SendJSON(statusCode int, payload any) {
rctx.StatusCode = statusCode
buffer := bytes.NewBuffer(nil)
json.NewEncoder(buffer).Encode(payload)
rctx.Writer.Header().Set("Content-Type", "application/json")
size := strconv.FormatInt(int64(len(buffer.Bytes())), 10)
rctx.Writer.Header().Set("Content-Length", size)
rctx.Writer.WriteHeader(statusCode)
rctx.Writer.Write(buffer.Bytes())
}
func (rctx *Context) SendText(statusCode int, payload string) {
rctx.StatusCode = statusCode
size := strconv.FormatInt(int64(len(payload)), 10)
rctx.Writer.Header().Set("Content-Type", "text/plain")
rctx.Writer.Header().Set("Content-Length", size)
rctx.Writer.WriteHeader(statusCode)
rctx.Writer.Write([]byte(payload))
}
func (rctx *Context) SendBytes(statusCode int, ctype string, payload []byte) {
rctx.StatusCode = statusCode
size := strconv.FormatInt(int64(len(payload)), 10)
rctx.Writer.Header().Set("Content-Type", ctype)
rctx.Writer.Header().Set("Content-Length", size)
rctx.Writer.WriteHeader(statusCode)
rctx.Writer.Write(payload)
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
func NewCorsMiddleware() MiddlewareFunc {
mw := func(next Handler) Handler {
return newCorsHandler(next)
}
return mw
}
type corsHandler struct {
next Handler
}
func newCorsHandler(next Handler) *corsHandler {
return &corsHandler{
next: next,
}
}
func (hand corsHandler) ServeHTTP(ctx *Context) {
origin := ctx.Request.Header.Get("Origin")
if origin != "" {
ctx.SetHeader("Access-Control-Allow-Origin", origin)
ctx.SetHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE, PATCH")
ctx.SetHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
ctx.SetHeader("Access-Control-Max-Age", "86400")
ctx.SetHeader("Access-Control-Allow-Credentials", "true")
}
if ctx.Request.Method == "OPTIONS" {
return
}
hand.next.ServeHTTP(ctx)
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
func NewLoggingMiddleware(print func(string, ...any)) MiddlewareFunc {
mw := func(next Handler) Handler {
return newLoggingHandler(next, print)
}
return mw
}
type loggingHandler struct {
next Handler
printFunc func(string, ...any)
}
func newLoggingHandler(next Handler, print func(string, ...any)) *loggingHandler {
return &loggingHandler{
next: next,
printFunc: print,
}
}
func (logging loggingHandler) ServeHTTP(rctx *Context) {
logging.next.ServeHTTP(rctx)
cl := rctx.Writer.Header().Get("Content-Length")
logging.printFunc("%s %s %s %s %s %d", rctx.Request.RemoteAddr,
rctx.Request.Method, rctx.Request.URL.String(),
rctx.Request.Proto, cl, rctx.StatusCode)
}
+72
View File
@@ -0,0 +1,72 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"fmt"
"strings"
)
const (
compContextPlain int = iota
compContextRegex
startRegex byte = '{'
stopRegex byte = '}'
defaultRegexp = `[a-zA-Z0-9_\.]+`
)
func pathCompiler(path string) (string, error) {
var err error
res := make([]byte, 0)
var pos int = compContextPlain
var depth int = 0
pattern := make([]byte, 0)
for _, b := range []byte(path) {
switch pos {
case compContextPlain:
switch b {
case stopRegex:
depth -= 1
res = append(res, b)
case startRegex:
depth += 1
pos = compContextRegex // pattern started
pattern = make([]byte, 0)
default:
res = append(res, b)
}
case compContextRegex:
switch b {
case startRegex:
depth += 1
case stopRegex:
depth -= 1
if depth == 0 {
pattern = convertRegexp(pattern)
res = append(res, pattern...)
pos = compContextPlain // pattern ended
}
default:
pattern = append(pattern, b)
}
}
}
if depth != 0 {
err = fmt.Errorf("Unbalanced brackets into pattern")
}
return string(res), err
}
func convertRegexp(src []byte) []byte {
var res string
const patternSeps = ":"
parts := strings.SplitN(string(src), patternSeps, 2)
if len(parts) == 1 {
parts = append(parts, defaultRegexp)
}
res = fmt.Sprintf("(?<%s>%s)", parts[0], parts[1])
return []byte(res)
}
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"fmt"
"regexp"
"testing"
"github.com/stretchr/testify/require"
)
func tDebugf(msg string, args ...any) {
fmt.Printf("debug: ")
fmt.Printf(msg, args...)
fmt.Printf("\n")
}
func TestPatchCompilerA(t *testing.T) {
var err error
srcPath := `/v1/file/{collection:[a-zA-Z]+}/{name}`
reSource, err := pathCompiler(srcPath)
require.NoError(t, err)
tDebugf("re: %s\n", reSource)
re, err := regexp.Compile(reSource)
require.NoError(t, err)
reqPath := `/v1/file/foo/bare`
match := re.MatchString(reqPath)
require.True(t, match)
submatch := re.FindStringSubmatch(reqPath)
subnames := re.SubexpNames()
submap := make(map[string]string)
for i, val := range subnames {
tDebugf("subname: %d = %s", i, val)
}
for i, val := range submatch {
key := subnames[i]
if key != "" {
submap[key] = val
}
tDebugf("sub: %d = %s", i, val)
}
tDebugf("submap: %v", submap)
}
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"net/http"
"runtime/debug"
"time"
)
func NewRecoveryMiddleware(print func(string, ...any)) MiddlewareFunc {
mw := func(next Handler) Handler {
return newRecoveryHandler(next, print)
}
return mw
}
type recoveryHandler struct {
next Handler
print func(string, ...any)
}
func newRecoveryHandler(next Handler, print func(string, ...any)) *recoveryHandler {
return &recoveryHandler{
next: next,
print: print,
}
}
func (hand recoveryHandler) ServeHTTP(rctx *Context) {
exitFunc := func() {
err := recover()
if err != nil {
rctx.Writer.WriteHeader(http.StatusInternalServerError)
stack := string(debug.Stack())
timestamp := time.Now().Format(time.RFC3339)
hand.print("%s %v ; %s\n", timestamp, err, stack)
}
}
defer exitFunc()
hand.next.ServeHTTP(rctx)
}
+166
View File
@@ -0,0 +1,166 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"fmt"
"net/http"
"regexp"
)
type MiddlewareFunc func(next Handler) Handler
type Handler interface {
ServeHTTP(rctx *Context)
}
type HandlerFunc func(rctx *Context)
func (handlerFunc HandlerFunc) ServeHTTP(rctx *Context) {
handlerFunc(rctx)
}
type Router struct {
headHandler Handler
routeHandler *Selector
}
func NewRouter() *Router {
selector := NewSelector()
return &Router{
routeHandler: selector,
headHandler: selector,
}
}
func (rout *Router) Use(mwFunc MiddlewareFunc) {
rout.headHandler = mwFunc(rout.headHandler)
}
func (rout *Router) Selector() *Selector {
return rout.routeHandler
}
func (rout *Router) AddRoute(method, path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute(method, path, handlerFunc)
}
func (rout *Router) Head(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("HEAD", path, handlerFunc)
}
func (rout *Router) Get(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("GET", path, handlerFunc)
}
func (rout *Router) Post(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("POST", path, handlerFunc)
}
func (rout *Router) Put(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("PUT", path, handlerFunc)
}
func (rout *Router) Patch(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("PATCH", path, handlerFunc)
}
func (rout *Router) Delete(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("DELETE", path, handlerFunc)
}
func (rout *Router) Connect(path string, handlerFunc HandlerFunc) {
rout.routeHandler.AddRoute("CONNECT", path, handlerFunc)
}
func (rout *Router) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
rctx := NewContext(writer, req)
rout.headHandler.ServeHTTP(rctx)
}
func (rout *Router) NotFound(handlerFunc HandlerFunc) {
rout.routeHandler.notFound = handlerFunc
}
type Selector struct {
Routes []*Route
notFound Handler
}
func NewSelector() *Selector {
notFound := HandlerFunc(func(ctx *Context) {
http.NotFound(ctx.Writer, ctx.Request)
})
return &Selector{
Routes: make([]*Route, 0),
notFound: notFound,
}
}
const globalRoutePattern = `^%s$`
func (hand *Selector) AddRoute(method, path string, handlerFunc HandlerFunc) error {
var err error
rawPath := path
path, err = pathCompiler(path)
if err != nil {
return err
}
path = fmt.Sprintf(globalRoutePattern, path)
re, err := regexp.Compile(path)
if err != nil {
return err
}
route := &Route{
Method: method,
Path: path,
RawPath: rawPath,
Handler: handlerFunc,
Regexp: re,
}
hand.Routes = append(hand.Routes, route)
return err
}
func (hand *Selector) ServeHTTP(rctx *Context) {
realHandler := hand.notFound
for _, route := range hand.Routes {
match := route.Match(rctx.Request)
if !match {
continue
}
subvals := route.Regexp.FindStringSubmatch(rctx.Request.URL.Path)
subkeys := route.Regexp.SubexpNames()
for i, val := range subvals {
key := subkeys[i]
if key != "" {
rctx.PathMap[key] = val
}
}
realHandler = route.Handler
break
}
realHandler.ServeHTTP(rctx)
}
type Route struct {
Method string
Regexp *regexp.Regexp
Path string
RawPath string
Handler HandlerFunc
}
func (route Route) Match(req *http.Request) bool {
if req.Method != route.Method {
return false
}
match := route.Regexp.MatchString(req.URL.Path)
if match {
return true
}
return false
}
+195
View File
@@ -0,0 +1,195 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package router
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestRouterStatus(t *testing.T) {
reqPath := "/hello"
handler := NewRouter()
helloHandler := func(rctx *Context) {
rctx.SetStatus(http.StatusOK)
}
handler.Get(reqPath, helloHandler)
request, err := http.NewRequest("GET", reqPath, nil)
require.NoError(t, err)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
fmt.Printf("Response code: %d\n", recorder.Code)
}
func TestRouterSendText(t *testing.T) {
testText := "hello, world"
reqPath := "/hello"
rout := NewRouter()
helloHandler := func(rctx *Context) {
rctx.SendText(http.StatusOK, testText)
}
rout.Get(reqPath, helloHandler)
request, err := http.NewRequest("GET", reqPath, nil)
require.NoError(t, err)
recorder := httptest.NewRecorder()
rout.ServeHTTP(recorder, request)
fmt.Printf("Response code: %d\n", recorder.Code)
require.Equal(t, http.StatusOK, recorder.Code)
bodyReader := recorder.Body
bodyBytes, err := io.ReadAll(bodyReader)
fmt.Printf("Response body: %s\n", string(bodyBytes))
require.Equal(t, string(bodyBytes), testText)
}
func TestRouterSendJSON(t *testing.T) {
type testStruct struct {
Message string `json:"message"`
Code int64 `json:"code"`
}
testVar := testStruct{
Message: "hello, world",
Code: 123,
}
testData, err := json.Marshal(testVar)
require.NoError(t, err)
reqPath := "/hello"
handler := NewRouter()
helloHandler := func(rctx *Context) {
rctx.SendJSON(http.StatusOK, &testVar)
}
handler.Get(reqPath, helloHandler)
request, err := http.NewRequest("GET", reqPath, nil)
require.NoError(t, err)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
fmt.Printf("Response code: %d\n", recorder.Code)
require.Equal(t, http.StatusOK, recorder.Code)
bodyReader := recorder.Body
bodyBytes, err := io.ReadAll(bodyReader)
require.NoError(t, err)
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
fmt.Printf("Response body: %s\n", string(bodyBytes))
require.Equal(t, string(testData), string(bodyBytes))
}
func TestRouterBindJSON(t *testing.T) {
type testStruct struct {
Message string `json:"message"`
Code int64 `json:code"`
}
testVar := testStruct{
Message: "hello, world",
Code: 123,
}
testData, err := json.Marshal(testVar)
require.NoError(t, err)
buffer := bytes.NewBuffer(testData)
reqPath := "/hello"
handler := NewRouter()
helloHandler := func(rctx *Context) {
handVar := testStruct{}
rctx.BindJSON(&handVar)
fmt.Printf("Received message: %s - %d\n", handVar.Message, handVar.Code)
require.Equal(t, handVar.Code, int64(123))
rctx.SendJSON(http.StatusOK, &handVar)
}
handler.Post(reqPath, helloHandler)
request, err := http.NewRequest("POST", reqPath, buffer)
require.NoError(t, err)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
fmt.Printf("Response code: %d\n", recorder.Code)
require.Equal(t, http.StatusOK, recorder.Code)
bodyReader := recorder.Body
bodyBytes, err := io.ReadAll(bodyReader)
require.NoError(t, err)
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
fmt.Printf("Response body: %s\n", string(bodyBytes))
require.Equal(t, string(testData), string(bodyBytes))
}
func TestRouterBindParams(t *testing.T) {
reqPath := "/hello"
handler := NewRouter()
helloHandler := func(rctx *Context) {
type Params struct {
Name string `param:"name"`
Code int64 `param:"code"`
}
params := &Params{}
rctx.BindQuery(params)
fmt.Printf("Received name: %s\n", params.Name)
fmt.Printf("Received code: %d\n", params.Code)
rctx.SendText(http.StatusOK, "hello")
}
handler.Get(reqPath, helloHandler)
reqPath = reqPath + `?name=world&code=123`
request, err := http.NewRequest("GET", reqPath, nil)
require.NoError(t, err)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
fmt.Printf("Response code: %d\n", recorder.Code)
require.Equal(t, http.StatusOK, recorder.Code)
bodyReader := recorder.Body
bodyBytes, err := io.ReadAll(bodyReader)
require.NoError(t, err)
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
fmt.Printf("Response body: %s\n", string(bodyBytes))
}
func BenchmarkLoggerL(b *testing.B) {
reqPath := "/hello"
helloHandler := func(rctx *Context) {
rctx.SetStatus(http.StatusOK)
}
handler := NewRouter()
handler.Get(reqPath, helloHandler)
request, err := http.NewRequest("GET", reqPath, nil)
require.NoError(b, err)
for i := 0; i < b.N; i++ {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
require.Equal(b, http.StatusOK, recorder.Code)
}
}
+418
View File
@@ -0,0 +1,418 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*
*
*/
package server
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"os/user"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
"mproxy/app/config"
"mproxy/app/handler"
"mproxy/app/logger"
"mproxy/app/service"
"mproxy/pkg/auxtool"
"mproxy/app/proxoper"
"mproxy/app/servoper"
)
type Server struct {
conf *config.Config
seop *servoper.Operator
prop *proxoper.Operator
svc *service.Service
hand *handler.Handler
logg *logger.Logger
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
logf *os.File
//x509cert []byte
//x509key []byte
listen net.Listener
}
func NewServer() (*Server, error) {
var err error
srv := &Server{}
srv.logg = logger.NewLoggerWithSubject("server")
srv.ctx, srv.cancel = context.WithCancel(context.Background())
return srv, err
}
func (srv *Server) Handler() *handler.Handler {
return srv.hand
}
func (srv *Server) Service() *service.Service {
return srv.svc
}
func (srv *Server) SetLogdir(dir string) {
srv.conf.Logpath = dir
}
func (srv *Server) SetRundir(dir string) {
srv.conf.Runpath = dir
}
func (srv *Server) SetDatadir(dir string) {
srv.conf.Database.Basepath = dir
srv.conf.Storage.Basepath = dir
srv.conf.Datadir = dir
}
func (srv *Server) SetPort(port uint32) {
srv.conf.Service.Port = port
}
func (srv *Server) SetAsDaemon(asDaemon bool) {
srv.conf.AsDaemon = asDaemon
}
func (srv *Server) Configure() error {
var err error
//srv.logg.Infof("Configuration server")
srv.conf = config.NewConfig()
if err != nil {
return err
}
err = srv.conf.ReadConfigfile()
if err != nil {
srv.logg.Warningf("Error loading config file: %v", err)
err = nil
}
err = srv.conf.ReadX509Cert()
if err != nil {
return err
}
return err
}
func (srv *Server) Build() error {
var err error
//srv.logg.Infof("Server building")
currUser, err := user.Current()
if err != nil {
err = fmt.Errorf("Error getting current user: %v\n", err)
return err
}
cuid64, err := strconv.ParseInt(currUser.Uid, 10, 64)
if err != nil {
return err
}
cgid64, err := strconv.ParseInt(currUser.Gid, 10, 64)
if err != nil {
return err
}
euid := int(cuid64)
egid := int(cgid64)
if cuid64 == 0 {
usr, err := user.Lookup(srv.conf.RunUser)
if err != nil {
return err
}
uid64, err := strconv.ParseInt(usr.Uid, 10, 64)
if err != nil {
return err
}
gid64, err := strconv.ParseInt(usr.Gid, 10, 64)
if err != nil {
return err
}
euid = int(uid64)
egid = int(gid64)
}
// Creating datadir
datadir := srv.conf.Datadir
if !auxtool.DirExists(datadir) { // TODO: check access to dir
//srv.logg.Infof("Creating data directory %s ", datadir)
err = os.MkdirAll(datadir, 0750)
if err != nil {
return err
}
}
err = os.Chown(datadir, euid, egid)
if err != nil {
return err
}
if srv.conf.AsDaemon {
logdir := filepath.Dir(srv.conf.Logpath)
//srv.logg.Infof("Creating log directory %s", logdir)
err = os.MkdirAll(logdir, 0750)
if err != nil {
return err
}
err = os.Chown(logdir, euid, egid)
if err != nil {
return err
}
rundir := filepath.Dir(srv.conf.Runpath)
//srv.logg.Infof("Creating run directory %s", rundir)
err = os.MkdirAll(rundir, 0750)
if err != nil {
return err
}
err = os.Chown(rundir, euid, egid)
if err != nil {
return err
}
// Redirect stderr and stout
logFile, err := os.OpenFile(srv.conf.Logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
if err != nil {
return err
}
err = syscall.Dup2(int(logFile.Fd()), int(os.Stdout.Fd()))
if err != nil {
return err
}
err = syscall.Dup2(int(logFile.Fd()), int(os.Stderr.Fd()))
if err != nil {
return err
}
srv.logf = logFile
}
confDump := srv.conf.String()
srv.logg.Infof("Current server configuration is:\n%s\n", confDump)
if cuid64 == 0 {
// Change effective user and group
err = syscall.Setgid(egid)
if err != nil {
return err
}
err = syscall.Setuid(euid)
if err != nil {
return err
}
}
//return fmt.Errorf("Debug break")
uidstr := strconv.FormatInt(int64(syscall.Geteuid()), 10)
usr, err := user.LookupId(uidstr)
if err != nil {
return err
}
srv.logg.Warningf("Now run as user: %s", usr.Username)
// Creating service operator
srv.logg.Infof("Creating operator")
servoperParams := &servoper.OperatorParams{}
srv.seop, err = servoper.NewOperator(servoperParams)
if err != nil {
return err
}
// Creating proxy operator
srv.logg.Infof("Creating operator")
proxoperParams := &proxoper.OperatorParams{}
srv.prop, err = proxoper.NewOperator(proxoperParams)
if err != nil {
return err
}
// Creating handler
srv.logg.Infof("Creating handler")
handlerParams := &handler.HandlerParams{
ServOper: srv.seop,
ProxOper: srv.prop,
}
srv.hand, err = handler.NewHandler(handlerParams)
if err != nil {
return err
}
// Creating service
serviceParams := &service.ServiceParams{
Handler: srv.hand,
X509cert: srv.conf.X509Cert,
X509key: srv.conf.X509Key,
Address: srv.conf.Service.Address,
Portnum: srv.conf.Service.Port,
}
srv.logg.Infof("Creating service")
srv.svc, err = service.NewService(serviceParams)
if err != nil {
return err
}
// Building service
err = srv.svc.Build()
if err != nil {
return err
}
return err
}
func (srv *Server) Run() error {
var err error
if srv.conf.AsDaemon {
// Redirect stdin
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
return err
}
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
if err != nil {
return err
}
// Write process ID
pidFile, err := os.OpenFile(srv.conf.Runpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer pidFile.Close()
currPid := os.Getpid()
_, err = pidFile.WriteString(strconv.Itoa(currPid))
if err != nil {
return err
}
// Start log rotator
srv.Rotator()
}
currUser, err := user.Current()
if err != nil {
return err
}
srv.logg.Infof("Server started with user: %s", currUser.Username)
uidstr := strconv.FormatInt(int64(syscall.Geteuid()), 10)
usr, err := user.LookupId(uidstr)
if err != nil {
return err
}
srv.logg.Infof("Server run with user: %s", usr.Username)
svcDone := make(chan error, 1)
// Service run
srv.logg.Infof("Start service")
startService := func(svc *service.Service, done chan error) {
err = svc.Run()
if err != nil {
srv.logg.Errorf("Service error: %v", err)
done <- err
}
}
go startService(srv.svc, svcDone)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
var signal os.Signal
select {
case signal = <-sigs:
srv.logg.Infof("Services stopped by signal: %v", signal)
srv.cancel()
srv.svc.Stop()
srv.wg.Wait()
case err = <-svcDone:
srv.logg.Infof("Service stopped by service error: %v", err)
srv.cancel()
srv.svc.Stop()
srv.wg.Wait()
}
return err
}
func (srv *Server) PseudoFork() error {
const successExit int = 0
var keyEnv string = "IMX0LTSELMRF8K"
var err error
_, isChild := os.LookupEnv(keyEnv)
switch {
case !isChild:
os.Setenv(keyEnv, "TRUE")
procAttr := syscall.ProcAttr{}
cwd, err := os.Getwd()
if err != nil {
return err
}
var sysFiles = make([]uintptr, 3)
sysFiles[0] = uintptr(syscall.Stdin)
sysFiles[1] = uintptr(syscall.Stdout)
sysFiles[2] = uintptr(syscall.Stderr)
procAttr.Files = sysFiles
procAttr.Env = os.Environ()
procAttr.Dir = cwd
_, err = syscall.ForkExec(os.Args[0], os.Args, &procAttr)
if err != nil {
return err
}
os.Exit(successExit)
case isChild:
_, err = syscall.Setsid()
if err != nil {
return err
}
}
os.Unsetenv(keyEnv)
return err
}
func (srv *Server) Daemonize() error {
var err error
if srv.conf.AsDaemon {
// Restart process process
err = srv.PseudoFork()
if err != nil {
return err
}
}
return err
}
func (srv *Server) Rotator() {
srv.wg.Add(1)
var counter uint64
logFunc := func() {
for {
counter += 1
select {
case <-srv.ctx.Done():
srv.wg.Done()
srv.logg.Infof("Log file rotator done")
return
default:
}
if (counter % 60) == 1 {
stat, err := srv.logf.Stat()
if err == nil && stat.Size() > srv.conf.LogLimit {
srv.logg.Infof("Rotate log file")
countFiles := 3
for i := 1; i < countFiles; i++ {
nextName := fmt.Sprintf("%s.%d", srv.conf.Logpath, i+1)
prevName := fmt.Sprintf("%s.%d", srv.conf.Logpath, i)
os.Rename(prevName, nextName)
}
os.Rename(srv.conf.Logpath, srv.conf.Logpath+".1")
logFile, err := os.OpenFile(srv.conf.Logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
if err == nil {
syscall.Dup2(int(logFile.Fd()), int(os.Stdout.Fd()))
syscall.Dup2(int(logFile.Fd()), int(os.Stderr.Fd()))
srv.logf.Close()
srv.logf = logFile
}
}
}
time.Sleep(1 * time.Second)
}
}
go logFunc()
}
+136
View File
@@ -0,0 +1,136 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package service
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
"mproxy/app/handler"
"mproxy/app/logger"
"mproxy/app/router"
)
type ServiceParams struct {
Handler *handler.Handler
Address string
Portnum uint32
X509cert string
X509key string
}
type Service struct {
hand *handler.Handler
rout *router.Router
logg *logger.Logger
hsrv *http.Server
address string
portnum uint32
listen net.Listener
x509cert string
x509key string
protocol string
}
func NewService(params *ServiceParams) (*Service, error) {
var err error
svc := &Service{
hand: params.Handler,
address: params.Address,
portnum: params.Portnum,
x509cert: params.X509cert,
x509key: params.X509key,
protocol: "TCP",
}
svc.logg = logger.NewLoggerWithSubject("service")
return svc, err
}
func (svc *Service) Build() error {
var err error
svc.logg.Infof("Service build ")
svc.rout = router.NewRouter()
svc.rout.Use(router.NewRecoveryMiddleware(svc.logg.Errorf))
svc.rout.Use(router.NewLoggingMiddleware(svc.logg.Infof))
svc.rout.Use(router.NewCorsMiddleware())
svc.rout.Use(svc.hand.AuthMiddleware)
svc.rout.Get(`/v3/api/service/hello`, svc.hand.SendHello)
svc.rout.Connect(`{hostaddr}`, svc.hand.ConnectTo)
svc.rout.NotFound(svc.hand.NotFound)
selector := svc.rout.Selector()
for _, item := range selector.Routes {
svc.logg.Infof("%s\t%s", item.Method, item.RawPath)
}
const useTLS = true
if useTLS {
tlsCert, err := tls.X509KeyPair([]byte(svc.x509cert), []byte(svc.x509key))
if err != nil {
return err
}
tlsConfig := tls.Config{
Certificates: []tls.Certificate{tlsCert},
ClientAuth: tls.NoClientCert,
InsecureSkipVerify: true,
}
listenAddress := fmt.Sprintf("%s:%d", svc.address, svc.portnum)
svc.listen, err = tls.Listen(svc.protocol, listenAddress, &tlsConfig)
if err != nil {
return err
}
} else {
listenAddress := fmt.Sprintf("%s:%d", svc.address, svc.portnum)
svc.listen, err = net.Listen(svc.protocol, listenAddress)
if err != nil {
return err
}
}
svc.logg.Infof("Service listening at %v", svc.listen.Addr())
svc.hsrv = &http.Server{
Handler: svc.rout,
}
return err
}
func (svc *Service) Run() error {
var err error
svc.logg.Infof("Service run")
err = svc.hsrv.Serve(svc.listen)
if err == http.ErrServerClosed {
svc.logg.Warningf("Service Closed")
err = nil
}
if err != nil {
return err
}
return err
}
func (svc *Service) Stop() {
if svc.hsrv != nil {
svc.logg.Infof("Service stop")
downWaiting := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), downWaiting)
defer cancel()
err := svc.hsrv.Shutdown(ctx)
if err != nil {
svc.logg.Errorf("Error service shutdown: %v", err)
}
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package servoper
import (
"mproxy/app/logger"
)
type OperatorParams struct{}
type Operator struct {
logg *logger.Logger
}
func NewOperator(params *OperatorParams) (*Operator, error) {
var err error
oper := &Operator{}
oper.logg = logger.NewLoggerWithSubject("servoper")
return oper, err
}
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*/
package servoper
type SendHelloParams struct{}
type SendHelloResult struct {
Message string `json:"message"`
Alive bool `json:"alive"`
}
func (oper *Operator) SendHello(param *SendHelloParams) (*SendHelloResult, error) {
var err error
res := &SendHelloResult{
Alive: true,
Message: "hello",
}
return res, err
}