Compare commits

..

No commits in common. "main" and "v2.8.11" have entirely different histories.

22 changed files with 174 additions and 151 deletions

View file

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View file

@ -15,13 +15,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: | images: |
hsanaeii/3x-ui hsanaeii/3x-ui
@ -32,28 +32,28 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
with: with:
install: true install: true
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View file

@ -2,9 +2,11 @@ name: Release 3X-UI
on: on:
workflow_dispatch: workflow_dispatch:
release:
types: [published]
push: push:
branches: branches:
- '**' - main
tags: tags:
- "v*.*.*" - "v*.*.*"
paths: paths:
@ -18,48 +20,9 @@ on:
- 'x-ui.service.debian' - 'x-ui.service.debian'
- 'x-ui.service.arch' - 'x-ui.service.arch'
- 'x-ui.service.rhel' - 'x-ui.service.rhel'
pull_request:
jobs: jobs:
analyze:
name: Analyze Go code
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files are not gofmt-formatted:"
echo "$unformatted"
exit 1
fi
- name: Run go vet
run: go vet ./...
- name: Run staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: "latest"
install-go: false
- name: Run tests
run: go test -race -shuffle=on ./...
build: build:
needs: analyze
permissions: permissions:
contents: write contents: write
strategy: strategy:
@ -75,7 +38,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@ -170,17 +133,19 @@ jobs:
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: x-ui-linux-${{ matrix.platform }} name: x-ui-linux-${{ matrix.platform }}
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
- name: Upload files to GH release - name: Upload files to GH release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }} tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
overwrite: true overwrite: true
@ -191,7 +156,6 @@ jobs:
# ================================= # =================================
build-windows: build-windows:
name: Build for Windows name: Build for Windows
needs: analyze
permissions: permissions:
contents: write contents: write
strategy: strategy:
@ -201,7 +165,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@ -266,17 +230,19 @@ jobs:
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: x-ui-windows-amd64 name: x-ui-windows-amd64
path: ./x-ui-windows-amd64.zip path: ./x-ui-windows-amd64.zip
- name: Upload files to GH release - name: Upload files to GH release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }} tag: ${{ github.ref }}
file: x-ui-windows-amd64.zip file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip
overwrite: true overwrite: true

View file

@ -1,7 +1,6 @@
package controller package controller
import ( import (
"fmt"
"net/http" "net/http"
"text/template" "text/template"
"time" "time"
@ -72,22 +71,14 @@ func (a *IndexController) login(c *gin.Context) {
return return
} }
user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode) user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
safeUser := template.HTMLEscapeString(form.Username) safeUser := template.HTMLEscapeString(form.Username)
safePass := template.HTMLEscapeString(form.Password) safePass := template.HTMLEscapeString(form.Password)
if user == nil { if user == nil {
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c)) logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
notifyPass := safePass
if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
notifyPass = fmt.Sprintf("*** (%s)", translatedError)
}
a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
} }

View file

@ -10,6 +10,7 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"sort" "sort"
"strconv"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database"
@ -318,14 +319,13 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
} }
} }
// Convert back to slice and sort by timestamp (oldest first) // Convert back to slice and sort by timestamp (newest first)
// This ensures we always protect the original/current connections and ban new excess ones.
allIps := make([]IPWithTimestamp, 0, len(ipMap)) allIps := make([]IPWithTimestamp, 0, len(ipMap))
for ip, timestamp := range ipMap { for ip, timestamp := range ipMap {
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp}) allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
} }
sort.Slice(allIps, func(i, j int) bool { sort.Slice(allIps, func(i, j int) bool {
return allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first) return allIps[i].Timestamp > allIps[j].Timestamp // Descending order (newest first)
}) })
shouldCleanLog := false shouldCleanLog := false
@ -345,17 +345,23 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
if len(allIps) > limitIp { if len(allIps) > limitIp {
shouldCleanLog = true shouldCleanLog = true
// Keep the oldest IPs (currently active connections) and ban the new excess ones. // Keep only the newest IPs (up to limitIp)
keptIps := allIps[:limitIp] keptIps := allIps[:limitIp]
bannedIps := allIps[limitIp:] disconnectedIps := allIps[limitIp:]
// Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z // Log the disconnected IPs (old ones)
for _, ipTime := range bannedIps { for _, ipTime := range disconnectedIps {
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP) j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp) log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
} }
// Update database with only the currently active (kept) IPs // Actually disconnect old IPs by temporarily removing and re-adding user
// This forces Xray to drop existing connections from old IPs
if len(disconnectedIps) > 0 {
j.disconnectClientTemporarily(inbound, clientEmail, clients)
}
// Update database with only the newest IPs
jsonIps, _ := json.Marshal(keptIps) jsonIps, _ := json.Marshal(keptIps)
inboundClientIps.Ips = string(jsonIps) inboundClientIps.Ips = string(jsonIps)
} else { } else {
@ -372,12 +378,67 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
} }
if len(j.disAllowedIps) > 0 { if len(j.disAllowedIps) > 0 {
logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps)) logger.Infof("[LIMIT_IP] Client %s: Kept %d newest IPs, disconnected %d old IPs", clientEmail, limitIp, len(j.disAllowedIps))
} }
return shouldCleanLog return shouldCleanLog
} }
// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
var xrayAPI xray.XrayAPI
// Get panel settings for API port
db := database.GetDB()
var apiPort int
var apiPortSetting model.Setting
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
apiPort, _ = strconv.Atoi(apiPortSetting.Value)
}
if apiPort == 0 {
apiPort = 10085 // Default API port
}
err := xrayAPI.Init(apiPort)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err)
return
}
defer xrayAPI.Close()
// Find the client config
var clientConfig map[string]any
for _, client := range clients {
if client.Email == clientEmail {
// Convert client to map for API
clientBytes, _ := json.Marshal(client)
json.Unmarshal(clientBytes, &clientConfig)
break
}
}
if clientConfig == nil {
return
}
// Remove user to disconnect all connections
err = xrayAPI.RemoveUser(inbound.Tag, clientEmail)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
return
}
// Wait a moment for disconnection to take effect
time.Sleep(100 * time.Millisecond)
// Re-add user to allow new connections
err = xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
}
}
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
inbound := &model.Inbound{} inbound := &model.Inbound{}

View file

@ -2032,6 +2032,7 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
return nil, err return nil, err
} }
if t != nil && client != nil { if t != nil && client != nil {
t.Enable = client.Enable
t.UUID = client.ID t.UUID = client.ID
t.SubId = client.SubID t.SubId = client.SubID
return t, nil return t, nil

View file

@ -1926,8 +1926,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} else { } else {
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
t.sendClientIndividualLinks(chatId, client_Email)
t.sendClientQRLinks(chatId, client_Email)
} }
case "add_client_submit_enable": case "add_client_submit_enable":
client_Enable = true client_Enable = true
@ -1938,8 +1936,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} else { } else {
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
t.sendClientIndividualLinks(chatId, client_Email)
t.sendClientQRLinks(chatId, client_Email)
} }
case "reset_all_traffics_cancel": case "reset_all_traffics_cancel":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
@ -3306,9 +3302,23 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
} }
} }
// getCommonClientButtons returns the shared inline keyboard rows for client configuration // addClient handles the process of adding a new client to an inbound.
func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton { func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
return [][]telego.InlineKeyboardButton{ inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
if err != nil {
t.SendMsgToTgbot(chatId, err.Error())
return
}
protocol := inbound.Protocol
switch protocol {
case model.VMESS, model.VLESS:
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
),
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
@ -3324,52 +3334,68 @@ func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
), ),
} )
} if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
// addClient handles the process of adding a new client to an inbound. } else {
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
if err != nil {
t.SendMsgToTgbot(chatId, err.Error())
return
}
protocol := inbound.Protocol
var protocolRows [][]telego.InlineKeyboardButton
switch protocol {
case model.VMESS, model.VLESS:
protocolRows = [][]telego.InlineKeyboardButton{
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
),
} }
case model.Trojan: case model.Trojan:
protocolRows = [][]telego.InlineKeyboardButton{ inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"),
), ),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
} }
case model.Shadowsocks: case model.Shadowsocks:
protocolRows = [][]telego.InlineKeyboardButton{ inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"),
), ),
} tu.InlineKeyboardRow(
} tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
commonRows := t.getCommonClientButtons() ),
inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...) tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
),
)
if len(messageID) > 0 { if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else { } else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard) t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
} }
}
} }

View file

@ -33,7 +33,7 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
return user, nil return user, nil
} }
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) { func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User {
db := database.GetDB() db := database.GetDB()
user := &model.User{} user := &model.User{}
@ -43,16 +43,17 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
First(user). First(user).
Error Error
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
return nil, errors.New("invalid credentials") return nil
} else if err != nil { } else if err != nil {
logger.Warning("check user err:", err) logger.Warning("check user err:", err)
return nil, err return nil
} }
// If LDAP enabled and local password check fails, attempt LDAP auth
if !crypto.CheckPasswordHash(user.Password, password) { if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable() ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled { if !ldapEnabled {
return nil, errors.New("invalid credentials") return nil
} }
host, _ := s.settingService.GetLdapHost() host, _ := s.settingService.GetLdapHost()
@ -76,14 +77,15 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
} }
ok, err := ldaputil.AuthenticateUser(cfg, username, password) ok, err := ldaputil.AuthenticateUser(cfg, username, password)
if err != nil || !ok { if err != nil || !ok {
return nil, errors.New("invalid credentials") return nil
} }
// On successful LDAP auth, continue 2FA checks below
} }
twoFactorEnable, err := s.settingService.GetTwoFactorEnable() twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil { if err != nil {
logger.Warning("check two factor err:", err) logger.Warning("check two factor err:", err)
return nil, err return nil
} }
if twoFactorEnable { if twoFactorEnable {
@ -91,15 +93,15 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
if err != nil { if err != nil {
logger.Warning("check two factor token err:", err) logger.Warning("check two factor token err:", err)
return nil, err return nil
} }
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
return nil, errors.New("invalid 2fa code") return nil
} }
} }
return user, nil return user
} }
func (s *UserService) UpdateUser(id int, username string, password string) error { func (s *UserService) UpdateUser(id int, username string, password string) error {

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ حفظت بيانات مستخدم Telegram." "userSaved" = "✅ حفظت بيانات مستخدم Telegram."
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n" "loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n" "loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
"2faFailed" = "فشل 2FA"
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n" "report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n" "datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n" "hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Telegram User saved." "userSaved" = "✅ Telegram User saved."
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n" "loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
"loginFailed" = "❗Login attempt to the panel failed.\r\n" "loginFailed" = "❗Login attempt to the panel failed.\r\n"
"2faFailed" = "2FA Failed"
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n" "report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n" "datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Usuario de Telegram guardado." "userSaved" = "✅ Usuario de Telegram guardado."
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n" "loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n" "loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
"2faFailed" = "Error de 2FA"
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n" "report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n" "datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n" "hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ کاربر تلگرام ذخیره شد." "userSaved" = "✅ کاربر تلگرام ذخیره شد."
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n" "loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n" "loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n"
"2faFailed" = "خطای 2FA"
"report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n" "report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n" "datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n"
"hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n" "hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Pengguna Telegram tersimpan." "userSaved" = "✅ Pengguna Telegram tersimpan."
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n" "loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n" "loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
"2faFailed" = "2FA Gagal"
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n" "report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n" "datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Telegramユーザーが保存されました。" "userSaved" = "✅ Telegramユーザーが保存されました。"
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n" "loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n" "loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
"2faFailed" = "2FAエラー"
"report" = "🕰 定期報告:{{ .RunTime }}\r\n" "report" = "🕰 定期報告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n" "datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n" "hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Usuário do Telegram salvo." "userSaved" = "✅ Usuário do Telegram salvo."
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n" "loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
"loginFailed" = "❗Tentativa de login no painel falhou.\r\n" "loginFailed" = "❗Tentativa de login no painel falhou.\r\n"
"2faFailed" = "Falha no 2FA"
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n" "report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n" "datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n"

View file

@ -149,7 +149,7 @@
"geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы." "geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
"geofilesUpdateAll" = "Обновить все" "geofilesUpdateAll" = "Обновить все"
"geofileUpdatePopover" = "Геофайлы успешно обновлены" "geofileUpdatePopover" = "Геофайл успешно обновлён"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу" "dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал" "logs" = "Журнал"
"config" = "Конфигурация" "config" = "Конфигурация"
@ -663,7 +663,6 @@
"userSaved" = "✅ Пользователь Telegram сохранен." "userSaved" = "✅ Пользователь Telegram сохранен."
"loginSuccess" = "✅ Успешный вход в панель.\r\n" "loginSuccess" = "✅ Успешный вход в панель.\r\n"
"loginFailed" = "❗️ Ошибка входа в панель.\r\n" "loginFailed" = "❗️ Ошибка входа в панель.\r\n"
"2faFailed" = "Ошибка 2FA"
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n" "report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n" "datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n" "hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi." "userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n" "loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
"loginFailed" = "❗Panele giriş denemesi başarısız oldu.\r\n" "loginFailed" = "❗Panele giriş denemesi başarısız oldu.\r\n"
"2faFailed" = "2FA Hatası"
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n" "report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n" "datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n" "hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Користувача Telegram збережено." "userSaved" = "✅ Користувача Telegram збережено."
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n" "loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
"loginFailed" = "❗️ Помилка входу в панель.\r\n" "loginFailed" = "❗️ Помилка входу в панель.\r\n"
"2faFailed" = "Помилка 2FA"
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n" "report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n" "datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
"hostname" = "💻 Хост: {{ .Hostname }}\r\n" "hostname" = "💻 Хост: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ Người dùng Telegram đã được lưu." "userSaved" = "✅ Người dùng Telegram đã được lưu."
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n" "loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n" "loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
"2faFailed" = "Lỗi 2FA"
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n" "report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n" "datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n" "hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ 电报用户已保存。" "userSaved" = "✅ 电报用户已保存。"
"loginSuccess" = "✅ 成功登录到面板。\r\n" "loginSuccess" = "✅ 成功登录到面板。\r\n"
"loginFailed" = "❗️ 面板登录失败。\r\n" "loginFailed" = "❗️ 面板登录失败。\r\n"
"2faFailed" = "2FA 失败"
"report" = "🕰 定时报告:{{ .RunTime }}\r\n" "report" = "🕰 定时报告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n" "datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
"hostname" = "💻 主机名:{{ .Hostname }}\r\n" "hostname" = "💻 主机名:{{ .Hostname }}\r\n"

View file

@ -663,7 +663,6 @@
"userSaved" = "✅ 電報使用者已儲存。" "userSaved" = "✅ 電報使用者已儲存。"
"loginSuccess" = "✅ 成功登入到面板。\r\n" "loginSuccess" = "✅ 成功登入到面板。\r\n"
"loginFailed" = "❗️ 面板登入失敗。\r\n" "loginFailed" = "❗️ 面板登入失敗。\r\n"
"2faFailed" = "2FA 失敗"
"report" = "🕰 定時報告:{{ .RunTime }}\r\n" "report" = "🕰 定時報告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n" "datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
"hostname" = "💻 主機名:{{ .Hostname }}\r\n" "hostname" = "💻 主機名:{{ .Hostname }}\r\n"

View file

@ -317,12 +317,12 @@ check_config() {
start >/dev/null 2>&1 start >/dev/null 2>&1
else else
LOGE "IP certificate setup failed." LOGE "IP certificate setup failed."
echo -e "${yellow}You can try again via option 19 (SSL Certificate Management).${plain}" echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}"
start >/dev/null 2>&1 start >/dev/null 2>&1
fi fi
else else
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
echo -e "${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}" echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}"
fi fi
fi fi
} }
@ -2012,7 +2012,7 @@ EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition] [Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*Disconnecting OLD IP\s*=\s*<ADDR>\s*\|\|\s*Timestamp\s*=\s*\d+ failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex = ignoreregex =
EOF EOF