Update On Fri Apr 25 14:33:06 CEST 2025

This commit is contained in:
github-action[bot]
2025-04-25 14:33:07 +02:00
parent 2ac24642d2
commit 05a86235e7
465 changed files with 8693 additions and 4758 deletions

View File

@@ -32,7 +32,7 @@ PROJECT_NAME=$(shell basename "${ROOT}")
# - pkg/version/current.go
#
# Use `tools/bump_version.sh` script to change all those files at one shot.
VERSION="3.13.1"
VERSION="3.14.1"
# Build binaries and installation packages.
.PHONY: build

View File

@@ -1,5 +1,5 @@
Package: mieru
Version: 3.13.1
Version: 3.14.1
Section: net
Priority: optional
Architecture: amd64

View File

@@ -1,5 +1,5 @@
Name: mieru
Version: 3.13.1
Version: 3.14.1
Release: 1%{?dist}
Summary: Mieru proxy client
License: GPLv3+

View File

@@ -1,5 +1,5 @@
Package: mieru
Version: 3.13.1
Version: 3.14.1
Section: net
Priority: optional
Architecture: arm64

View File

@@ -1,5 +1,5 @@
Name: mieru
Version: 3.13.1
Version: 3.14.1
Release: 1%{?dist}
Summary: Mieru proxy client
License: GPLv3+

View File

@@ -1,5 +1,5 @@
Package: mita
Version: 3.13.1
Version: 3.14.1
Section: net
Priority: optional
Architecture: amd64

View File

@@ -1,5 +1,5 @@
Name: mita
Version: 3.13.1
Version: 3.14.1
Release: 1%{?dist}
Summary: Mieru proxy server
License: GPLv3+

View File

@@ -1,5 +1,5 @@
Package: mita
Version: 3.13.1
Version: 3.14.1
Section: net
Priority: optional
Architecture: arm64

View File

@@ -1,5 +1,5 @@
Name: mita
Version: 3.13.1
Version: 3.14.1
Release: 1%{?dist}
Summary: Mieru proxy server
License: GPLv3+

View File

@@ -182,8 +182,6 @@ The format of the simple sharing link is as follows:
A simple sharing link starts with `mierus://`, where `s` stands for `simple`.
Some special characters are not allowed in username or password. In case a character is not allowed, the simple sharing link cannot be generated.
There is only one server address in a simple sharing link. If the client's configuration contains multiple servers, multiple links will be generated.
The supported parameters are:

View File

@@ -182,8 +182,6 @@ mierus://baozi:manlianpenfen@1.2.3.4?mtu=1400&multiplexing=MULTIPLEXING_HIGH&por
简单分享链接以 `mierus://` 开始,其中 `s` 表示 `simple`
用户名和密码不允许使用某些特殊字符。如果使用了不允许的字符,则无法生成简单分享链接。
简单分享链接中只有一个服务器地址。如果客户端的设置含有多台服务器,则会生成多个链接。
链接中支持的参数列表如下:

View File

@@ -8,32 +8,32 @@ Before installation and configuration, connect to the server via SSH and then ex
```sh
# Debian / Ubuntu - X86_64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita_3.13.1_amd64.deb
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita_3.14.1_amd64.deb
# Debian / Ubuntu - ARM 64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita_3.13.1_arm64.deb
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita_3.14.1_arm64.deb
# RedHat / CentOS / Rocky Linux - X86_64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita-3.13.1-1.x86_64.rpm
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita-3.14.1-1.x86_64.rpm
# RedHat / CentOS / Rocky Linux - ARM 64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita-3.13.1-1.aarch64.rpm
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita-3.14.1-1.aarch64.rpm
```
## Install mita package
```sh
# Debian / Ubuntu - X86_64
sudo dpkg -i mita_3.13.1_amd64.deb
sudo dpkg -i mita_3.14.1_amd64.deb
# Debian / Ubuntu - ARM 64
sudo dpkg -i mita_3.13.1_arm64.deb
sudo dpkg -i mita_3.14.1_arm64.deb
# RedHat / CentOS / Rocky Linux - X86_64
sudo rpm -Uvh --force mita-3.13.1-1.x86_64.rpm
sudo rpm -Uvh --force mita-3.14.1-1.x86_64.rpm
# RedHat / CentOS / Rocky Linux - ARM 64
sudo rpm -Uvh --force mita-3.13.1-1.aarch64.rpm
sudo rpm -Uvh --force mita-3.14.1-1.aarch64.rpm
```
Those instructions can also be used to upgrade the version of mita software package.

View File

@@ -8,32 +8,32 @@
```sh
# Debian / Ubuntu - X86_64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita_3.13.1_amd64.deb
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita_3.14.1_amd64.deb
# Debian / Ubuntu - ARM 64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita_3.13.1_arm64.deb
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita_3.14.1_arm64.deb
# RedHat / CentOS / Rocky Linux - X86_64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita-3.13.1-1.x86_64.rpm
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita-3.14.1-1.x86_64.rpm
# RedHat / CentOS / Rocky Linux - ARM 64
curl -LSO https://github.com/enfein/mieru/releases/download/v3.13.1/mita-3.13.1-1.aarch64.rpm
curl -LSO https://github.com/enfein/mieru/releases/download/v3.14.1/mita-3.14.1-1.aarch64.rpm
```
## 安装 mita 软件包
```sh
# Debian / Ubuntu - X86_64
sudo dpkg -i mita_3.13.1_amd64.deb
sudo dpkg -i mita_3.14.1_amd64.deb
# Debian / Ubuntu - ARM 64
sudo dpkg -i mita_3.13.1_arm64.deb
sudo dpkg -i mita_3.14.1_arm64.deb
# RedHat / CentOS / Rocky Linux - X86_64
sudo rpm -Uvh --force mita-3.13.1-1.x86_64.rpm
sudo rpm -Uvh --force mita-3.14.1-1.x86_64.rpm
# RedHat / CentOS / Rocky Linux - ARM 64
sudo rpm -Uvh --force mita-3.13.1-1.aarch64.rpm
sudo rpm -Uvh --force mita-3.14.1-1.aarch64.rpm
```
上述指令也可以用来升级 mita 软件包的版本。

View File

@@ -353,6 +353,7 @@ func GetServerStatusWithRPC(ctx context.Context) (*pb.AppStatusMsg, error) {
}
// IsServerDaemonRunning returns nil if app status shows server daemon is running.
// It returns nil even if the proxy function is not running.
func IsServerDaemonRunning(appStatus *pb.AppStatusMsg) error {
if appStatus == nil {
return fmt.Errorf("AppStatusMsg is nil")

View File

@@ -20,7 +20,6 @@ import (
"fmt"
"net"
"net/url"
"regexp"
"strconv"
"strings"
@@ -30,14 +29,6 @@ import (
"google.golang.org/protobuf/proto"
)
const (
safeURLPattern = `^[0-9A-Za-z_!\$&'\(\)\*\+,;=\.\~-]+$`
)
var (
safeURLRegExp = regexp.MustCompile(safeURLPattern)
)
// ClientConfigToURL creates a URL to share the client configuration.
func ClientConfigToURL(config *pb.ClientConfig) (string, error) {
if config == nil {
@@ -66,15 +57,9 @@ func ClientProfileToMultiURLs(profile *pb.ClientProfile) (urls []string, err err
if userName == "" {
return nil, fmt.Errorf("user name in profile %s is empty", profileName)
}
if !isSafeURLString(userName) {
return nil, fmt.Errorf(`user name %q in profile %s can't be safely encoded to URL. Allowed pattern: %s`, userName, profileName, safeURLPattern)
}
if password == "" {
return nil, fmt.Errorf("password in profile %s is empty", profileName)
}
if !isSafeURLString(password) {
return nil, fmt.Errorf(`password %q in profile %s can't be safely encoded to URL. Allowed pattern: %s`, password, profileName, safeURLPattern)
}
if len(servers) == 0 {
return nil, fmt.Errorf("profile %s has no server", profileName)
}
@@ -243,7 +228,3 @@ func URLToClientProfile(s string) (*pb.ClientProfile, error) {
p.Servers = append(p.Servers, server)
return p, nil
}
func isSafeURLString(input string) bool {
return safeURLRegExp.MatchString(input)
}

View File

@@ -28,8 +28,8 @@ func TestClientConfigWithURL(t *testing.T) {
{
ProfileName: proto.String("default"),
User: &pb.User{
Name: proto.String("abcABC123_!$&'()*+,;=.~-"),
Password: proto.String("defDEF456_!$&'()*+,;=.~-"),
Name: proto.String("abcABC123:<>[]{}|_ !$&'()*+,;=.~-"),
Password: proto.String("defDEF456:<>[]{}|_ !$&'()*+,;=.~-"),
},
Servers: []*pb.ServerEndpoint{
{
@@ -71,8 +71,8 @@ func TestClientProfileWithMultiURLs(t *testing.T) {
p := &pb.ClientProfile{
ProfileName: proto.String("default"),
User: &pb.User{
Name: proto.String("abcABC123_!$&'()*+,;=.~-"),
Password: proto.String("defDEF456_!$&'()*+,;=.~-"),
Name: proto.String("abcABC123:<>[]{}|_ !$&'()*+,;=.~-"),
Password: proto.String("defDEF456:<>[]{}|_ !$&'()*+,;=.~-"),
},
Servers: []*pb.ServerEndpoint{
{
@@ -107,8 +107,8 @@ func TestClientProfileWithMultiURLs(t *testing.T) {
p0 := &pb.ClientProfile{
ProfileName: proto.String("default"),
User: &pb.User{
Name: proto.String("abcABC123_!$&'()*+,;=.~-"),
Password: proto.String("defDEF456_!$&'()*+,;=.~-"),
Name: proto.String("abcABC123:<>[]{}|_ !$&'()*+,;=.~-"),
Password: proto.String("defDEF456:<>[]{}|_ !$&'()*+,;=.~-"),
},
Servers: []*pb.ServerEndpoint{
{
@@ -134,8 +134,8 @@ func TestClientProfileWithMultiURLs(t *testing.T) {
p1 := &pb.ClientProfile{
ProfileName: proto.String("default"),
User: &pb.User{
Name: proto.String("abcABC123_!$&'()*+,;=.~-"),
Password: proto.String("defDEF456_!$&'()*+,;=.~-"),
Name: proto.String("abcABC123:<>[]{}|_ !$&'()*+,;=.~-"),
Password: proto.String("defDEF456:<>[]{}|_ !$&'()*+,;=.~-"),
},
Servers: []*pb.ServerEndpoint{
{
@@ -177,56 +177,3 @@ func TestClientProfileWithMultiURLs(t *testing.T) {
t.Errorf("profile is not equal after generating and loading URL %q", urls[1])
}
}
func TestIsSafeURLString(t *testing.T) {
testCases := []struct {
input string
isSafe bool
}{
{input: "abc", isSafe: true},
{input: "ABC", isSafe: true},
{input: "123", isSafe: true},
{input: "_", isSafe: true},
{input: "!", isSafe: true},
{input: "$", isSafe: true},
{input: "&", isSafe: true},
{input: "'", isSafe: true},
{input: "(", isSafe: true},
{input: ")", isSafe: true},
{input: "*", isSafe: true},
{input: "+", isSafe: true},
{input: ",", isSafe: true},
{input: ";", isSafe: true},
{input: "=", isSafe: true},
{input: ".", isSafe: true},
{input: "~", isSafe: true},
{input: "-", isSafe: true},
{input: "abcABC123_!$&'()*+,;=.~-", isSafe: true},
{input: " ", isSafe: false},
{input: "\"", isSafe: false},
{input: "#", isSafe: false},
{input: "%", isSafe: false},
{input: "/", isSafe: false},
{input: "\\", isSafe: false},
{input: ":", isSafe: false},
{input: "<", isSafe: false},
{input: ">", isSafe: false},
{input: "?", isSafe: false},
{input: "@", isSafe: false},
{input: "[", isSafe: false},
{input: "]", isSafe: false},
{input: "^", isSafe: false},
{input: "`", isSafe: false},
{input: "{", isSafe: false},
{input: "|", isSafe: false},
{input: "}", isSafe: false},
{input: "abc 123", isSafe: false},
}
for _, tc := range testCases {
actual := isSafeURLString(tc.input)
if actual != tc.isSafe {
t.Errorf("isSafeURLString(%q) = %v, want %v", tc.input, actual, tc.isSafe)
}
}
}

View File

@@ -154,6 +154,20 @@ func RegisterServerCommands() {
},
serverGetConnectionsFunc,
)
RegisterCallback(
[]string{"", "get", "users"},
func(s []string) error {
return unexpectedArgsError(s, 3)
},
serverGetUsersFunc,
)
RegisterCallback(
[]string{"", "get", "quotas"},
func(s []string) error {
return unexpectedArgsError(s, 3)
},
serverGetQuotasFunc,
)
RegisterCallback(
[]string{"", "get", "thread-dump"},
func(s []string) error {
@@ -248,6 +262,14 @@ var serverHelpFunc = func(s []string) error {
cmd: "get connections",
help: []string{"Get mita server connections."},
},
{
cmd: "get users",
help: []string{"Get mita server registered users."},
},
{
cmd: "get quotas",
help: []string{"Get mita server user quotas."},
},
{
cmd: "version",
help: []string{"Show mita server version."},
@@ -744,6 +766,162 @@ var serverGetConnectionsFunc = func(s []string) error {
return nil
}
var serverGetUsersFunc = func(_ []string) error {
appStatus, err := appctl.GetServerStatusWithRPC(context.Background())
if err != nil {
if stderror.IsConnRefused(err) {
return fmt.Errorf(stderror.ServerNotRunningWithCommand)
}
return fmt.Errorf(stderror.GetServerStatusFailedErr, err)
}
if err := appctl.IsServerDaemonRunning(appStatus); err != nil {
return fmt.Errorf(stderror.ServerNotRunningErr, err)
}
client, err := appctl.NewServerManagementRPCClient()
if err != nil {
return fmt.Errorf(stderror.CreateServerManagementRPCClientFailedErr, err)
}
timedctx, cancelFunc := context.WithTimeout(context.Background(), appctl.RPCTimeout)
defer cancelFunc()
userWithMetricsList, err := client.GetUsers(timedctx, &appctlpb.Empty{})
if err != nil {
return fmt.Errorf(stderror.GetUsersFailedErr, err)
}
header := []string{
"User",
"LastActive",
"1DayDownload",
"1DayUpload",
"30DaysDownload",
"30DaysUpload",
}
table := make([][]string, 0)
table = append(table, header)
for _, userWithMetrics := range userWithMetricsList.GetItems() {
row := make([]string, 6)
row[0] = userWithMetrics.GetUser().GetName()
// Collect download and upload metrics of this user.
var down, up *metrics.Counter
var err error
for _, metric := range userWithMetrics.GetMetrics() {
switch metric.GetName() {
case metrics.UserMetricDownloadBytes:
down, err = metrics.NewCounterFromMetricPB(metric)
if err != nil {
return fmt.Errorf("metrics.NewCounterFromMetricPB() failed: %w", err)
}
case metrics.UserMetricUploadBytes:
up, err = metrics.NewCounterFromMetricPB(metric)
if err != nil {
return fmt.Errorf("metrics.NewCounterFromMetricPB() failed: %w", err)
}
}
}
var lastDownloadTime, lastUploadTime time.Time
if down != nil {
lastDownloadTime = down.LastUpdateTime()
row[2] = common.ByteCountIEC(down.DeltaBetween(time.Now().Add(-24*time.Hour), time.Now()))
row[4] = common.ByteCountIEC(down.DeltaBetween(time.Now().Add(-720*time.Hour), time.Now()))
} else {
row[2] = "-"
row[4] = "-"
}
if up != nil {
lastUploadTime = up.LastUpdateTime()
row[3] = common.ByteCountIEC(up.DeltaBetween(time.Now().Add(-24*time.Hour), time.Now()))
row[5] = common.ByteCountIEC(up.DeltaBetween(time.Now().Add(-720*time.Hour), time.Now()))
} else {
row[3] = "-"
row[5] = "-"
}
if lastDownloadTime.IsZero() && lastUploadTime.IsZero() {
row[1] = "-"
} else if lastDownloadTime.After(lastUploadTime) {
row[1] = lastDownloadTime.Format(time.RFC3339)
} else {
row[1] = lastUploadTime.Format(time.RFC3339)
}
table = append(table, row)
}
printTable(table, " ")
return nil
}
var serverGetQuotasFunc = func(_ []string) error {
appStatus, err := appctl.GetServerStatusWithRPC(context.Background())
if err != nil {
if stderror.IsConnRefused(err) {
return fmt.Errorf(stderror.ServerNotRunningWithCommand)
}
return fmt.Errorf(stderror.GetServerStatusFailedErr, err)
}
if err := appctl.IsServerDaemonRunning(appStatus); err != nil {
return fmt.Errorf(stderror.ServerNotRunningErr, err)
}
client, err := appctl.NewServerManagementRPCClient()
if err != nil {
return fmt.Errorf(stderror.CreateServerManagementRPCClientFailedErr, err)
}
timedctx, cancelFunc := context.WithTimeout(context.Background(), appctl.RPCTimeout)
defer cancelFunc()
userWithMetricsList, err := client.GetUsers(timedctx, &appctlpb.Empty{})
if err != nil {
return fmt.Errorf(stderror.GetUsersFailedErr, err)
}
header := []string{
"User",
"Days",
"Limit",
"Usage",
}
table := make([][]string, 0)
table = append(table, header)
for _, userWithMetrics := range userWithMetricsList.GetItems() {
if len(userWithMetrics.GetUser().GetQuotas()) == 0 {
continue
}
// Collect download and upload metrics of this user.
var down, up *metrics.Counter
var err error
for _, metric := range userWithMetrics.GetMetrics() {
switch metric.GetName() {
case metrics.UserMetricDownloadBytes:
down, err = metrics.NewCounterFromMetricPB(metric)
if err != nil {
return fmt.Errorf("metrics.NewCounterFromMetricPB() failed: %w", err)
}
case metrics.UserMetricUploadBytes:
up, err = metrics.NewCounterFromMetricPB(metric)
if err != nil {
return fmt.Errorf("metrics.NewCounterFromMetricPB() failed: %w", err)
}
}
}
if down == nil || up == nil {
continue
}
for _, quota := range userWithMetrics.GetUser().GetQuotas() {
row := make([]string, 4)
row[0] = userWithMetrics.GetUser().GetName()
row[1] = strconv.Itoa(int(quota.GetDays()))
row[2] = common.ByteCountIEC(int64(quota.GetMegabytes()) * 1048576)
row[3] = common.ByteCountIEC(down.DeltaBetween(time.Now().Add(-24*time.Duration(quota.GetDays())*time.Hour), time.Now()) + up.DeltaBetween(time.Now().Add(-24*time.Duration(quota.GetDays())*time.Hour), time.Now()))
table = append(table, row)
}
}
printTable(table, " ")
return nil
}
var serverGetThreadDumpFunc = func(s []string) error {
appStatus, err := appctl.GetServerStatusWithRPC(context.Background())
if err != nil {

View File

@@ -53,15 +53,6 @@ func printSessionInfoList(info *appctlpb.SessionInfoList) {
"LastRecv",
"LastSend",
}
idLen := len(header[0])
protocolLen := len(header[1])
localAddrLen := len(header[2])
remoteAddrLen := len(header[3])
stateLen := len(header[4])
recvQBufLen := len(header[5])
sendQBufLen := len(header[6])
lastRecvLen := len(header[7])
lastSendLen := len(header[8])
// Map the SessionInfo object to fields, and record the length of the fields.
table := make([][]string, 0)
@@ -70,20 +61,12 @@ func printSessionInfoList(info *appctlpb.SessionInfoList) {
row := make([]string, 9)
row[0] = si.GetId()
idLen = mathext.Max(idLen, len(row[0]))
row[1] = si.GetProtocol()
protocolLen = mathext.Max(protocolLen, len(row[1]))
row[2] = si.GetLocalAddr()
localAddrLen = mathext.Max(localAddrLen, len(row[2]))
row[3] = si.GetRemoteAddr()
remoteAddrLen = mathext.Max(remoteAddrLen, len(row[3]))
row[4] = si.GetState()
stateLen = mathext.Max(stateLen, len(row[4]))
row[5] = fmt.Sprintf("%d+%d", si.GetRecvQ(), si.GetRecvBuf())
recvQBufLen = mathext.Max(recvQBufLen, len(row[5]))
row[6] = fmt.Sprintf("%d+%d", si.GetSendQ(), si.GetSendBuf())
sendQBufLen = mathext.Max(sendQBufLen, len(row[6]))
lastRecvTime := time.Unix(si.LastRecvTime.GetSeconds(), int64(si.LastRecvTime.GetNanos()))
if si.GetProtocol() == "TCP" {
@@ -91,27 +74,46 @@ func printSessionInfoList(info *appctlpb.SessionInfoList) {
} else {
row[7] = fmt.Sprintf("%v (%d)", time.Since(lastRecvTime).Truncate(time.Second), si.GetLastRecvSeq())
}
lastRecvLen = mathext.Max(lastRecvLen, len(row[7]))
lastSendTime := time.Unix(si.LastSendTime.GetSeconds(), int64(si.LastSendTime.GetNanos()))
row[8] = fmt.Sprintf("%v (%d)", time.Since(lastSendTime).Truncate(time.Second), si.GetLastSendSeq())
lastSendLen = mathext.Max(lastSendLen, len(row[8]))
table = append(table, row)
}
// Pad the length of each row and print.
delim := " "
printTable(table, " ")
}
func printTable(table [][]string, delim string) {
nRow := len(table)
if nRow == 0 {
return
}
nCol := len(table[0])
if nCol == 0 {
return
}
// Verify each row has the same number of columns.
for _, row := range table {
rowWithPadding := make([]string, 9)
rowWithPadding[0] = fmt.Sprintf("%-"+fmt.Sprintf("%d", idLen)+"s", row[0])
rowWithPadding[1] = fmt.Sprintf("%-"+fmt.Sprintf("%d", protocolLen)+"s", row[1])
rowWithPadding[2] = fmt.Sprintf("%-"+fmt.Sprintf("%d", localAddrLen)+"s", row[2])
rowWithPadding[3] = fmt.Sprintf("%-"+fmt.Sprintf("%d", remoteAddrLen)+"s", row[3])
rowWithPadding[4] = fmt.Sprintf("%-"+fmt.Sprintf("%d", stateLen)+"s", row[4])
rowWithPadding[5] = fmt.Sprintf("%-"+fmt.Sprintf("%d", recvQBufLen)+"s", row[5])
rowWithPadding[6] = fmt.Sprintf("%-"+fmt.Sprintf("%d", sendQBufLen)+"s", row[6])
rowWithPadding[7] = fmt.Sprintf("%-"+fmt.Sprintf("%d", lastRecvLen)+"s", row[7])
rowWithPadding[8] = fmt.Sprintf("%-"+fmt.Sprintf("%d", lastSendLen)+"s", row[8])
if len(row) != nCol {
panic(fmt.Sprintf("when print table, row %v has %d columns, expect %d", row, len(row), nCol))
}
}
// Calculate the length that should occupy by each column.
lens := make([]int, nCol)
for _, row := range table {
for j, field := range row {
lens[j] = mathext.Max(lens[j], len(field))
}
}
// Print the table with padding.
for _, row := range table {
rowWithPadding := make([]string, nCol)
for j := range row {
rowWithPadding[j] = fmt.Sprintf("%-"+fmt.Sprintf("%d", lens[j])+"s", row[j])
}
log.Infof("%s", strings.Join(rowWithPadding, delim))
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (C) 2025 mieru authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package common
import "fmt"
// ByteCountIEC formats bytes as a human-readable string (IEC units).
func ByteCountIEC(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%dB", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f%ciB", float64(b)/float64(div), "KMGTP"[exp])
}

View File

@@ -100,7 +100,7 @@ func (f *DaemonFormatter) Format(entry *Entry) ([]byte, error) {
var value string
switch {
case key == FieldKeyTime:
value = entry.Time.Format(time.RFC3339)
value = entry.Time.Format(defaultTimestampFormat)
case key == FieldKeyLevel:
value = strings.ToUpper(entry.Level.String())
case key == FieldKeyMsg:

View File

@@ -110,6 +110,24 @@ func (c *Counter) DeltaBetween(t1, t2 time.Time) int64 {
return sum
}
// LastUpdateTime returns the last time when the history is updated.
func (c *Counter) LastUpdateTime() time.Time {
if !c.timeSeries {
panic(fmt.Sprintf("%s is not a time series Counter", c.name))
}
c.mu.Lock()
defer c.mu.Unlock()
c.op++
if len(c.history) == 0 {
return time.Time{}
}
if c.history[len(c.history)-1].TimeUnixMilli == nil {
return time.Time{}
}
return time.UnixMilli(*c.history[len(c.history)-1].TimeUnixMilli)
}
func (c *Counter) addWithTime(delta int64, time time.Time) int64 {
c.mu.Lock()
defer c.mu.Unlock()
@@ -118,6 +136,7 @@ func (c *Counter) addWithTime(delta int64, time time.Time) int64 {
if delta == 0 {
return c.value
}
c.value += delta
if c.timeSeries {
if c.history == nil {

View File

@@ -69,6 +69,10 @@ func TestCounter(t *testing.T) {
t.Errorf("DeltaBetween() = %v, want %v", value, tc.value)
}
}
lastUpdateTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)
if c.LastUpdateTime().Unix() != lastUpdateTime.Unix() {
t.Errorf("LastUpdateTime() = %v, want %v", c.LastUpdateTime(), lastUpdateTime)
}
}
func TestRollUp(t *testing.T) {

View File

@@ -130,7 +130,7 @@ func LoadMetricsFromDump() error {
metricName := pbMetric.GetName()
if metric, ok := group.GetMetric(metricName); ok {
if counter, ok := metric.(*Counter); ok {
fromMetricPB(counter, pbMetric)
loadCounterFromMetricPB(counter, pbMetric)
}
}
}
@@ -218,6 +218,22 @@ func ToMetricPB(src Metric) *pb.Metric {
return dst
}
// NewCounterFromMetricPB creates a counter metric from the protobuf.
func NewCounterFromMetricPB(src *pb.Metric) (*Counter, error) {
if src.GetType() != pb.MetricType_COUNTER && src.GetType() != pb.MetricType_COUNTER_TIME_SERIES {
return nil, fmt.Errorf("type %v can't be converted to Counter", src.GetType().String())
}
c := &Counter{
name: src.GetName(),
}
if src.GetType() == pb.MetricType_COUNTER_TIME_SERIES {
c.timeSeries = true
}
loadCounterFromMetricPB(c, src)
return c, nil
}
// LogMetricsNow writes the current metrics to log.
// This function can be called when (periodic) logging is disabled.
func LogMetricsNow() {
@@ -252,7 +268,7 @@ func logMetricsLoop() {
}
}
func fromMetricPB(dst *Counter, src *pb.Metric) {
func loadCounterFromMetricPB(dst *Counter, src *pb.Metric) {
// Verify the name and type matches.
if src.GetName() != dst.Name() {
return

View File

@@ -21,6 +21,9 @@ import (
"path/filepath"
"testing"
"time"
pb "github.com/enfein/mieru/v3/pkg/metrics/metricspb"
"google.golang.org/protobuf/proto"
)
func TestEnableAndDisableLogging(t *testing.T) {
@@ -58,3 +61,25 @@ func TestMetricsDump(t *testing.T) {
t.Fatalf("LoadMetricsFromDump(): %v", err)
}
}
func TestMetricPBConvertion(t *testing.T) {
p := &pb.Metric{
Name: proto.String("counter"),
Type: pb.MetricType_COUNTER_TIME_SERIES.Enum(),
Value: proto.Int64(100),
History: []*pb.History{
{
TimeUnixMilli: proto.Int64(time.Now().UnixMilli()),
Delta: proto.Int64(100),
},
},
}
c, err := NewCounterFromMetricPB(p)
if err != nil {
t.Fatalf("NewCounterFromMetricPB() failed: %v", err)
}
p2 := ToMetricPB(c)
if !proto.Equal(p, p2) {
t.Errorf("metric protobuf doesn't match")
}
}

View File

@@ -22,8 +22,8 @@ const (
const (
UserMetricGroupFormat = userMetricGroupPrefix + "%s"
UserMetricUploadBytes = "UploadBytes"
UserMetricDownloadBytes = "DownloadBytes"
UserMetricUploadBytes = "UploadBytes"
)
var (
@@ -39,12 +39,12 @@ var (
// Current number of established connections.
CurrEstablished = RegisterMetric("connections", "CurrEstablished", GAUGE)
// Number of bytes from client to server.
UploadBytes = RegisterMetric("traffic", "UploadBytes", COUNTER)
// Number of bytes from server to client.
DownloadBytes = RegisterMetric("traffic", "DownloadBytes", COUNTER)
// Number of bytes from client to server.
UploadBytes = RegisterMetric("traffic", "UploadBytes", COUNTER)
// Number of padding bytes send to proxy connections.
OutputPaddingBytes = RegisterMetric("traffic", "OutputPaddingBytes", COUNTER)
)

View File

@@ -35,6 +35,7 @@ const (
GetServerConfigFailedErr = "get mita server config failed: %w"
GetServerStatusFailedErr = "get mita server status failed: %w"
GetThreadDumpFailedErr = "get thread dump failed: %w"
GetUsersFailedErr = "get users failed: %w"
InvalidPortBindingsErr = "invalid port bindings: %w"
InvalidTransportProtocol = "invalid transport protocol"
IPAddressNotFound = "IP address not found from domain name %q"

View File

@@ -16,5 +16,5 @@
package version
const (
AppVersion = "3.13.1"
AppVersion = "3.14.1"
)

View File

@@ -112,6 +112,8 @@ fi
./mita get heap-profile /test/mita.tcp.heap.gz
# Print metrics and memory statistics.
./mita get users
sleep 1
print_mieru_client_metrics
sleep 1
print_mieru_server_metrics

View File

@@ -116,6 +116,8 @@ fi
./mita get heap-profile /test/mita.udp.heap.gz
# Print metrics and memory statistics.
./mita get users
sleep 1
print_mieru_client_metrics
sleep 1
print_mieru_server_metrics

View File

@@ -17,72 +17,218 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import json
import os
import platform
import re
import subprocess
import sys
import urllib.request
from typing import Any, List
def main() -> None:
check_platform()
check_version()
check_permission()
print(detect_package_system())
print(detect_cpu_arch())
sys_info = SysInfo()
installer = Installer()
print(installer.download_mita(sys_info))
def detect_package_system() -> str:
if is_deb():
return 'deb'
if is_rpm():
return 'rpm'
else:
class SysInfo:
def __init__(self) -> None:
self.check_python_version()
self.check_platform()
self.check_permission()
self.package_manager = self.detect_package_manager()
if self.package_manager == '':
print_exit('Failed to detect system package manager. Supported: deb, rpm.')
self.cpu_arch = self.detect_cpu_arch()
if self.cpu_arch == '':
print_exit('Failed to detect CPU architecture. Supported: amd64, arm64.')
self.is_mita_installed = self.detect_mita_installed()
self.installed_mita_version = None
self.is_mita_config_applied = False
if self.is_mita_installed:
version_out = run_command(['mita', 'version'], check=True)
version = Version()
if version.parse(version_out.stdout.strip()):
self.installed_mita_version = version
if os.path.exists('/etc/mita/server.conf.pb') and \
os.stat('/etc/mita/server.conf.pb').st_size > 0:
self.is_mita_config_applied = True
self.latest_mita_version = None
latest_mita_version_str = self.query_latest_mita_version()
version2 = Version()
if version2.parse(latest_mita_version_str):
self.latest_mita_version = version2
def check_python_version(self) -> None:
if sys.version_info < (3, 8, 0):
print_exit('Python version must be 3.8.0 or higher.')
def check_platform(self) -> None:
if not sys.platform.startswith('linux'):
print_exit('You can only run this program on Linux.')
def check_permission(self) -> None:
uid = os.getuid()
if uid != 0:
print_exit('Only root user can run this program.')
def detect_package_manager(self) -> str:
if self.is_deb():
return 'deb'
elif self.is_rpm():
return 'rpm'
else:
return ''
def is_deb(self) -> bool:
'''
Return true if system uses deb package.
'''
result = run_command(['dpkg', '-l'])
return result.returncode == 0 and len(result.stdout.splitlines()) > 1
def is_rpm(self) -> bool:
'''
Return true if system uses rpm package.
'''
result = run_command(['rpm', '-qa'])
return result.returncode == 0 and len(result.stdout.splitlines()) > 1
def detect_mita_installed(self) -> bool:
'''
Return true if mita deb or rpm package is installed.
'''
if self.is_deb():
result = run_command(['dpkg', '-l'])
for l in result.stdout.splitlines():
if "mita" in l:
return True
elif self.is_rpm():
result = run_command(['rpm', '-qa'])
for l in result.stdout.splitlines():
if "mita" in l:
return True
else:
return False
def detect_cpu_arch(self) -> str:
machine = platform.machine()
if machine == 'x86_64' or machine == 'AMD64':
return 'amd64'
elif machine == 'aarch64' or machine == 'arm64':
return 'arm64'
return ''
def is_deb() -> bool:
'''
Return true if system uses deb package.
'''
result = run_command(['dpkg', '-l'], timeout=15)
return result.returncode == 0 and len(result.stdout.splitlines()) > 1
def query_latest_mita_version(self) -> str:
'''
Use GitHub API to fetch the latest mita version.
'''
try:
resp = urllib.request.urlopen("https://api.github.com/repos/enfein/mieru/releases/latest")
body = resp.read()
j = json.loads(body.decode('utf-8'))
return j["tag_name"].strip('v')
except Exception as e:
print_exit(f"Failed to query latest mita version: {e}")
def is_rpm() -> bool:
'''
Return true if system uses rpm package.
'''
result = run_command(['rpm', '-qa'], timeout=15)
return result.returncode == 0 and len(result.stdout.splitlines()) > 1
class Version:
def __init__(self, major=None, minor=None, patch=None):
self.major = major
self.minor = minor
self.patch = patch
def detect_cpu_arch() -> str:
if is_amd64():
return 'amd64'
if is_arm64():
return 'arm64'
return ''
def __str__(self):
return f'{self.major}.{self.minor}.{self.patch}'
def is_amd64() -> bool:
'''
Return true if the CPU architecture is amd64.
'''
machine = platform.machine()
return machine == 'x86_64' or machine == 'AMD64'
def parse(self, v: str) -> bool:
match = re.match(r'(\d+)\.(\d+)\.(\d+)', v)
if match:
self.major = int(match.group(1))
self.minor = int(match.group(2))
self.patch = int(match.group(3))
return True
return False
def is_arm64() -> bool:
'''
Return true if the CPU architecture is arm64.
'''
machine = platform.machine()
return machine == 'aarch64' or machine == 'arm64'
def is_less_than(self, another) -> bool:
'''
Return true if this version is less than another version.
'''
if self.major is None or self.minor is None or self.patch is None or \
another.major is None or another.minor is None or another.patch is None:
return False # Handle uninitialized versions
if self.major < another.major:
return True
elif self.major > another.major:
return False
if self.minor < another.minor:
return True
elif self.minor > another.minor:
return False
if self.patch < another.patch:
return True
else:
return False
def run_command(args: List[str], input=None, timeout=None, check=False):
class Installer:
def download_mita(self, sys_info: SysInfo) -> str:
'''
Download mita deb or rpm installation package.
Return the path of downloaded file.
'''
if sys_info.latest_mita_version == None:
print_exit('Latest mita version is unknown')
ver = sys_info.latest_mita_version
download_url = ''
if sys_info.package_manager == 'deb' and sys_info.cpu_arch == 'amd64':
download_url = f'https://github.com/enfein/mieru/releases/download/v{ver}/mita_{ver}_amd64.deb'
elif sys_info.package_manager == 'deb' and sys_info.cpu_arch == 'arm64':
download_url = f'https://github.com/enfein/mieru/releases/download/v{ver}/mita_{ver}_arm64.deb'
elif sys_info.package_manager == 'rpm' and sys_info.cpu_arch == 'amd64':
download_url = f'https://github.com/enfein/mieru/releases/download/v{ver}/mita-{ver}-1.x86_64.rpm'
elif sys_info.package_manager == 'rpm' and sys_info.cpu_arch == 'arm64':
download_url = f'https://github.com/enfein/mieru/releases/download/v{ver}/mita-{ver}-1.aarch64.rpm'
else:
print_exit(f'Failed to determine download URL based on package manager {sys_info.package_manager} and CPU architecture {sys_info.cpu_arch}')
filename = os.path.join('/tmp', download_url.split('/')[-1])
try:
urllib.request.urlretrieve(download_url, filename)
except urllib.error.URLError as e:
print_exit(f'Failed to download {download_url}: {e}')
return filename
class Configurer:
pass
class Uninstaller:
pass
def run_command(args: List[str], input=None, timeout=10, check=False):
'''
Run the command and return a subprocess.CompletedProcess instance.
'''
@@ -101,22 +247,6 @@ def run_command(args: List[str], input=None, timeout=None, check=False):
return result
def check_platform() -> None:
if not sys.platform.startswith('linux'):
print_exit('You can only run this program on Linux.')
def check_version() -> None:
if sys.version_info < (3, 8, 0):
print_exit('Python version must be 3.8.0 or higher.')
def check_permission() -> None:
uid = os.getuid()
if uid != 0:
print_exit('Only root user can run this program.')
def print_exit(*values: Any) -> None:
print(*values)
sys.exit(1)

View File

@@ -91,7 +91,8 @@ def deb_uninstall() -> None:
run_command('[uninstall deb package]', ['dpkg', '-P', 'mita'])
run_command('[remove mita configuration]', ['rm', '-rf', '/etc/mita'])
run_command('[remove mita metrics]', ['rm', '-rf', '/var/lib/mita'])
run_command('[remove mita runtime]', ['rm', '-rf', '/var/run/mita'])
run_command('[remove mita runtime old]', ['rm', '-rf', '/var/run/mita.sock'])
run_command('[remove mita runtime new]', ['rm', '-rf', '/var/run/mita'])
run_command('[remove mita systemd unit]', ['rm', '-f', '/lib/systemd/system/mita.service'])
run_command('[remove TCP BBR sysctl patch]', ['rm', '-f', '/etc/sysctl.d/mieru_tcp_bbr.conf'])
run_command('[reload systemd]', ['systemctl', 'daemon-reload'])
@@ -105,7 +106,8 @@ def rpm_uninstall() -> None:
run_command('[remove mita configuration]', ['rm', '-rf', '/etc/mita'])
run_command('[remove mita mailbox]', ['rm', '-rf', '/var/spool/mail/mita'])
run_command('[remove mita metrics]', ['rm', '-rf', '/var/lib/mita'])
run_command('[remove mita runtime]', ['rm', '-rf', '/var/run/mita'])
run_command('[remove mita runtime old]', ['rm', '-rf', '/var/run/mita.sock'])
run_command('[remove mita runtime new]', ['rm', '-rf', '/var/run/mita'])
run_command('[remove mita systemd unit]', ['rm', '-f', '/lib/systemd/system/mita.service'])
run_command('[remove TCP BBR sysctl patch]', ['rm', '-f', '/etc/sysctl.d/mieru_tcp_bbr.conf'])
run_command('[reload systemd]', ['systemctl', 'daemon-reload'])