working commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user