Refactor: Implement universal panel migration architecture

- Created modular source panel interface for extensibility
- Renamed arguments from marzban-* to panel-*
- Added panel-type argument with support for "marzban" and "marzneshin"
- Implemented Marzneshin panel support with unique proxy extraction logic
- Restructured models to support different panel types
- Updated README with new options and instructions
- Renamed app to "remnawave-migrate" to reflect universal nature

This refactoring makes the migration tool more flexible and
allows easier addition of new panel types in the future.

Co-authored-by: Yury Kastov <kastov@gog.sh>
This commit is contained in:
Sergey Kutovoy 2025-03-17 00:48:02 +05:00
parent bceb7f6f24
commit faf2786d01
No known key found for this signature in database
GPG key ID: 485DE7FCA2B08DD2
17 changed files with 471 additions and 198 deletions

View file

@ -36,13 +36,12 @@ jobs:
- name: Setup Go Environment
uses: actions/setup-go@v5
with:
go-version-file: marzban/go.mod
go-version-file: go.mod
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
workdir: marzban
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}

View file

@ -1,6 +1,6 @@
version: 2
project_name: marzban-migration-tool
project_name: remnawave-migrate
before:
hooks:
@ -13,7 +13,7 @@ checksum:
builds:
- id: build
binary: marzban-migration-tool
binary: remnawave-migrate
env:
- CGO_ENABLED=0
goos:
@ -28,7 +28,7 @@ builds:
- -X main.version={{ .Tag }}
archives:
- id: marzban-migration-tool
- id: remnawave-migrate
format: tar.gz
name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
format_overrides:

135
README.md
View file

@ -1,42 +1,131 @@
# Remnawave Migration Tools
# Remnawave Migration Tool
This repository contains a collection of tools for migrating users from various VPN panels to Remnawave panel.
A command-line tool for migrating users from various VPN management panels to Remnawave.
## Available Migration Tools
## Supported Source Panels
### [Marzban Migration Tool](./marzban)
- Marzban
- Marzneshin
Migrate users from Marzban panel to Remnawave panel. Supports batch processing, selective migration of recent users, and custom traffic reset strategies.
## Overview
Features:
This tool helps you migrate user accounts from various VPN management panels to a Remnawave. It supports batch processing, selective migration of recent users, and customization of traffic reset strategies.
- Migrate user credentials and settings
- Batch processing
- Selective migration of recent users
Key features:
- Batch processing with configurable batch size
- Migration of selected number of most recent users
- Automatic handling of existing users
- Support for environment variables
- Customizable traffic reset strategy
- Environment variables support
- Flexible status handling
[Learn more about Marzban Migration →](./marzban)
### Migrated User Fields
## General Information
The following user fields are migrated to Remnawave:
All migration tools in this repository follow these principles:
| Field | Description |
| -------------------- | ------------------------------------------------- |
| Username | User's unique identifier |
| Status | User's status (can be preserved or set to ACTIVE) |
| ShortUUID | Generated from subscription URL hash |
| TrojanPassword | Password for Trojan protocol |
| VlessUUID | UUID for VLESS protocol |
| SsPassword | Password for Shadowsocks protocol |
| TrafficLimitBytes | Traffic limit in bytes |
| TrafficLimitStrategy | Traffic reset strategy (can be customized) |
| ExpireAt | Account expiration date (UTC) |
| Description | User notes/description |
- Safe and non-destructive migration
- Configurable through CLI flags and environment variables
- Detailed logging and error handling
- Respect for existing users and data
- Clear documentation and usage examples
## Configuration
## Contributing
The tool can be configured using command-line flags or environment variables:
We welcome contributions for new migration tools or improvements to existing ones. If you'd like to add support for migrating from another panel:
| Flag | Environment Variable | Description | Default |
| ---------------------- | -------------------- | ---------------------------------------------- | --------- |
| `--panel-type` | `PANEL_TYPE` | Source panel type (`marzban` or `marzneshin`) | `marzban` |
| `--panel-url` | `PANEL_URL` | Source panel URL | |
| `--panel-username` | `PANEL_USERNAME` | Source panel admin username | |
| `--panel-password` | `PANEL_PASSWORD` | Source panel admin password | |
| `--remnawave-url` | `REMNAWAVE_URL` | Destination panel URL | |
| `--remnawave-token` | `REMNAWAVE_TOKEN` | Destination panel API token | |
| `--batch-size` | `BATCH_SIZE` | Number of users to process in one batch | 100 |
| `--last-users` | `LAST_USERS` | Only migrate last N users (0 means all users) | 0 |
| `--preferred-strategy` | `PREFERRED_STRATEGY` | Preferred traffic reset strategy for all users | |
| `--preserve-status` | `PRESERVE_STATUS` | Preserve user status from source panel | false |
## Usage
### Basic Usage
```bash
# Migrate all users (sets all users to ACTIVE status)
./migration-tool \
--panel-type=marzban \
--panel-url="http://marzban.example.com" \
--panel-username="admin" \
--panel-password="password" \
--remnawave-url="http://remnawave.example.com" \
--remnawave-token="your-token"
```
### Preserve User Status
```bash
# Migrate users preserving their original status
./migration-tool \
[other flags...] \
--preserve-status
```
### Migrate Last N Users
```bash
# Migrate only the last 50 users
./migration-tool \
[other flags...] \
--last-users=50
```
### Set Preferred Traffic Reset Strategy
```bash
# Migrate users with a specific reset strategy
./migration-tool \
[other flags...] \
--preferred-strategy=MONTH
```
Available strategy values:
- `NO_RESET` - No traffic limit reset
- `DAY` - Reset daily
- `WEEK` - Reset weekly
- `MONTH` - Reset monthly
**Note:** If not specified, the original strategy from Marzban will be used (with YEAR strategy converted to NO_RESET as Remnawave doesn't support yearly resets).
### Using Environment Variables
```bash
export PANEL_TYPE="marzban"
export PANEL_URL="http://marzban.example.com"
export PANEL_USERNAME="admin"
export PANEL_PASSWORD="password"
export REMNAWAVE_URL="http://remnawave.example.com"
export REMNAWAVE_TOKEN="your-token"
export BATCH_SIZE="200"
export LAST_USERS="50"
export PREFERRED_STRATEGY="MONTH"
export PRESERVE_STATUS="true"
./migration-tool
```
## Contribute
1. **Fork & Branch**: Fork this repository and create a branch for your work.
2. **Create a new directory** for your panel tool (e.g., 3xui for 3X-UI migration)
3. **Follow the existing code structure** and documentation patterns
4. **Submit a pull request** with your changes
2. **Implement Changes**: Work on your feature or fix, keeping code clean and well-documented.
3. **Test**: Ensure your changes maintain or improve current functionality, adding tests for new features.
4. **Commit & PR**: Commit your changes with clear messages, then open a pull request detailing your work.
5. **Feedback**: Be prepared to engage with feedback and further refine your contribution.

View file

@ -3,22 +3,23 @@ package config
import "github.com/alecthomas/kong"
type Config struct {
MarzbanURL string `name:"marzban-url" help:"Marzban panel URL" required:"true" env:"MARZBAN_URL"`
MarzbanUsername string `name:"marzban-username" help:"Marzban admin username" required:"true" env:"MARZBAN_USERNAME"`
MarzbanPassword string `name:"marzban-password" help:"Marzban admin password" required:"true" env:"MARZBAN_PASSWORD"`
PanelType string `name:"panel-type" help:"Source panel type (e.g., marzban, marzneshin)" required:"true" default:"marzban" enum:"marzban,marzneshin" env:"PANEL_TYPE"`
PanelURL string `name:"panel-url" help:"Source panel URL" required:"true" env:"PANEL_URL"`
PanelUsername string `name:"panel-username" help:"Source panel admin username" required:"true" env:"PANEL_USERNAME"`
PanelPassword string `name:"panel-password" help:"Source panel admin password" required:"true" env:"PANEL_PASSWORD"`
RemnawaveURL string `name:"remnawave-url" help:"Destination panel URL" env:"REMNAWAVE_URL"`
RemnawaveToken string `name:"remnawave-token" help:"Destination panel API token" env:"REMNAWAVE_TOKEN"`
BatchSize int `name:"batch-size" help:"Number of users to process in one batch" default:"100" env:"BATCH_SIZE"`
LastUsers int `name:"last-users" help:"Only migrate last N users (0 means all users)" default:"0" env:"LAST_USERS"`
PreferredStrategy string `name:"preferred-strategy" help:"Preferred traffic reset strategy for all users (NO_RESET, DAY, WEEK, MONTH). If set, overrides the user's original strategy" default:"" env:"PREFERRED_STRATEGY"`
PreserveStatus bool `name:"preserve-status" help:"Preserve user status from Marzban (if false, sets all users to ACTIVE)" default:"false" env:"PRESERVE_STATUS"`
PreserveStatus bool `name:"preserve-status" help:"Preserve user status from source panel (if false, sets all users to ACTIVE)" default:"false" env:"PRESERVE_STATUS"`
}
func Parse(version string) *Config {
var cfg Config
kong.Parse(&cfg,
kong.Name("marzban-migration-tool"),
kong.Description("Migrate users from Marzban panel to Remnawave panel"),
kong.Name("remnawave-migrate"),
kong.Description("Migrate users from various panels to Remnawave panel"),
kong.Vars{"version": version},
)
return &cfg

View file

@ -1,4 +1,4 @@
module marzban-migration-tool
module remnawave-migrate
go 1.23.5

View file

@ -4,10 +4,10 @@ import (
"log"
"strings"
"marzban-migration-tool/config"
"marzban-migration-tool/marzban"
"marzban-migration-tool/migrator"
"marzban-migration-tool/remnawave"
"remnawave-migrate/config"
"remnawave-migrate/migrator"
"remnawave-migrate/remnawave"
"remnawave-migrate/source"
)
var (
@ -17,8 +17,8 @@ var (
func main() {
cfg := config.Parse(version)
if cfg.MarzbanPassword == "" {
log.Fatal("Marzban password is required")
if cfg.PanelPassword == "" {
log.Fatal("Panel password is required")
}
if cfg.RemnawaveToken == "" {
log.Fatal("Remnawave token is required")
@ -39,14 +39,20 @@ func main() {
cfg.PreferredStrategy = strategy
}
marzbanPanel := marzban.NewPanel(cfg.MarzbanURL)
if err := marzbanPanel.Login(cfg.MarzbanUsername, cfg.MarzbanPassword); err != nil {
log.Printf("Starting migration from %s panel...", cfg.PanelType)
sourcePanel, err := source.Factory(cfg.PanelType, cfg.PanelURL)
if err != nil {
log.Fatalf("Failed to create source panel: %v", err)
}
if err := sourcePanel.Login(cfg.PanelUsername, cfg.PanelPassword); err != nil {
log.Fatalf("Login failed: %v", err)
}
remnaPanel := remnawave.NewPanel(cfg.RemnawaveURL, cfg.RemnawaveToken)
m := migrator.New(marzbanPanel, remnaPanel, cfg.PreferredStrategy, cfg.PreserveStatus)
m := migrator.New(sourcePanel, remnaPanel, cfg.PreferredStrategy, cfg.PreserveStatus)
if err := m.MigrateUsers(cfg.BatchSize, cfg.LastUsers); err != nil {
log.Fatalf("Migration failed: %v", err)
}

View file

@ -1,123 +0,0 @@
# Marzban Migration Tool
A command-line tool for migrating users from Marzban panel to Remnawave panel.
## Overview
This tool helps you migrate user accounts from a Marzban VPN panel to a Remnawave panel. It supports batch processing, selective migration of recent users, and customization of traffic reset strategies.
Key features:
- Batch processing with configurable batch size
- Migration of selected number of most recent users
- Automatic handling of existing users
- Support for environment variables
- Customizable traffic reset strategy
- Flexible status handling
### Migrated User Fields
The following user fields are migrated from Marzban to Remnawave:
| Field | Description |
| -------------------- | ------------------------------------------------- |
| Username | User's unique identifier |
| Status | User's status (can be preserved or set to ACTIVE) |
| ShortUUID | Generated from subscription URL hash |
| TrojanPassword | Password for Trojan protocol |
| VlessUUID | UUID for VLESS protocol |
| SsPassword | Password for Shadowsocks protocol |
| TrafficLimitBytes | Traffic limit in bytes |
| TrafficLimitStrategy | Traffic reset strategy (can be customized) |
| ExpireAt | Account expiration date (UTC) |
| Description | User notes/description |
## Configuration
The tool can be configured using command-line flags or environment variables:
| Flag | Environment Variable | Description | Default |
| ---------------------- | -------------------- | --------------------------------------- | -------- |
| `--marzban-url` | `MARZBAN_URL` | Source Marzban panel URL | Required |
| `--marzban-username` | `MARZBAN_USERNAME` | Marzban admin username | Required |
| `--marzban-password` | `MARZBAN_PASSWORD` | Marzban admin password | Required |
| `--remnawave-url` | `REMNAWAVE_URL` | Destination Remnawave panel URL | Required |
| `--remnawave-token` | `REMNAWAVE_TOKEN` | Remnawave API token | Required |
| `--batch-size` | `BATCH_SIZE` | Number of users to process in one batch | 100 |
| `--last-users` | `LAST_USERS` | Only migrate last N users | 0 (all) |
| `--preferred-strategy` | `PREFERRED_STRATEGY` | Preferred traffic reset strategy | (empty) |
| `--preserve-status` | `PRESERVE_STATUS` | Preserve user status from Marzban | false |
## Usage
### Basic Usage
```bash
# Migrate all users (sets all users to ACTIVE status)
./marzban-migration-tool \
--marzban-url="http://marzban.example.com" \
--marzban-username="admin" \
--marzban-password="password" \
--remnawave-url="http://remnawave.example.com" \
--remnawave-token="your-token"
```
### Preserve User Status
```bash
# Migrate users preserving their original status
./marzban-migration-tool \
[other flags...] \
--preserve-status
```
### Migrate Last N Users
```bash
# Migrate only the last 50 users
./marzban-migration-tool \
[other flags...] \
--last-users=50
```
### Set Preferred Traffic Reset Strategy
```bash
# Migrate users with a specific reset strategy
./marzban-migration-tool \
[other flags...] \
--preferred-strategy=MONTH
```
Available strategy values:
- `NO_RESET` - No traffic limit reset
- `DAY` - Reset daily
- `WEEK` - Reset weekly
- `MONTH` - Reset monthly
**Note:** If not specified, the original strategy from Marzban will be used (with YEAR strategy converted to NO_RESET as Remnawave doesn't support yearly resets).
### Using Environment Variables
```bash
export MARZBAN_URL="http://marzban.example.com"
export MARZBAN_USERNAME="admin"
export MARZBAN_PASSWORD="password"
export REMNAWAVE_URL="http://remnawave.example.com"
export REMNAWAVE_TOKEN="your-token"
export BATCH_SIZE="200"
export LAST_USERS="50"
export PREFERRED_STRATEGY="MONTH"
export PRESERVE_STATUS="true"
./marzban-migration-tool
```
## Contribute
1. **Fork & Branch**: Fork this repository and create a branch for your work.
2. **Implement Changes**: Work on your feature or fix, keeping code clean and well-documented.
3. **Test**: Ensure your changes maintain or improve current functionality, adding tests for new features.
4. **Commit & PR**: Commit your changes with clear messages, then open a pull request detailing your work.
5. **Feedback**: Be prepared to engage with feedback and further refine your contribution.

View file

@ -4,18 +4,18 @@ import (
"fmt"
"log"
"marzban-migration-tool/marzban"
"marzban-migration-tool/remnawave"
"remnawave-migrate/remnawave"
"remnawave-migrate/source"
)
type Migrator struct {
source *marzban.Panel
source source.SourcePanel
destination *remnawave.Panel
PreferredStrategy string
PreserveStatus bool
}
func New(source *marzban.Panel, destination *remnawave.Panel, preferredStrategy string, preserveStatus bool) *Migrator {
func New(source source.SourcePanel, destination *remnawave.Panel, preferredStrategy string, preserveStatus bool) *Migrator {
return &Migrator{
source: source,
destination: destination,

View file

@ -1,11 +1,29 @@
package models
import (
"marzban-migration-tool/util"
"remnawave-migrate/util"
"strings"
"time"
)
type UsersResponse struct {
Users []User `json:"users"`
Total int `json:"total"`
}
type User struct {
MarzbanUser MarzbanUser
ProcessedUser ProcessedUser
}
func (u *User) Process() ProcessedUser {
if u.ProcessedUser.Username != "" {
return u.ProcessedUser
}
return u.MarzbanUser.Process()
}
type MarzbanProxies struct {
Vless struct {
ID string `json:"id"`
@ -32,6 +50,11 @@ type MarzbanUser struct {
SubscriptionURL string `json:"subscription_url"`
}
type MarzbanUsersResponse struct {
Users []MarzbanUser `json:"users"`
Total int `json:"total"`
}
type ProcessedUser struct {
Expire string `json:"expire"`
DataLimit int64 `json:"data_limit"`
@ -133,11 +156,6 @@ func (p *ProcessedUser) ToCreateUserRequest(preferredStrategy string, preserveSt
return req
}
type MarzbanUsersResponse struct {
Users []MarzbanUser `json:"users"`
Total int `json:"total"`
}
func strPtr(s string) *string {
if s == "" {
return nil

View file

@ -7,7 +7,7 @@ import (
"net/http"
"strings"
"marzban-migration-tool/models"
"remnawave-migrate/models"
)
type Panel struct {
@ -45,14 +45,14 @@ func (p *Panel) CreateUser(req models.CreateUserRequest) error {
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusBadRequest {
if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError {
body, _ := io.ReadAll(resp.Body)
var apiErr ApiError
if err := json.Unmarshal(body, &apiErr); err != nil {
return fmt.Errorf("failed to parse error response: %w", err)
}
if apiErr.ErrorCode == "A019" {
if apiErr.ErrorCode == "A019" || apiErr.ErrorCode == "A020" || apiErr.ErrorCode == "A021" || apiErr.ErrorCode == "A032" {
return &UserExistsError{
Username: req.Username,
ApiError: apiErr,

View file

@ -1,4 +1,4 @@
package marzban
package source
import (
"encoding/json"
@ -8,23 +8,23 @@ import (
"net/url"
"strings"
"marzban-migration-tool/models"
"remnawave-migrate/models"
)
type Panel struct {
type MarzbanPanel struct {
client *http.Client
baseURL string
authToken string
}
func NewPanel(baseURL string) *Panel {
return &Panel{
func NewMarzbanPanel(baseURL string) *MarzbanPanel {
return &MarzbanPanel{
client: &http.Client{},
baseURL: baseURL,
}
}
func (p *Panel) Login(username, password string) error {
func (p *MarzbanPanel) Login(username, password string) error {
data := url.Values{}
data.Set("username", username)
data.Set("password", password)
@ -61,7 +61,7 @@ func (p *Panel) Login(username, password string) error {
return nil
}
func (p *Panel) GetUsers(offset, limit int) (*models.MarzbanUsersResponse, error) {
func (p *MarzbanPanel) GetUsers(offset, limit int) (*models.UsersResponse, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("%s/api/users?offset=%d&limit=%d", p.baseURL, offset, limit),
nil)
@ -83,10 +83,21 @@ func (p *Panel) GetUsers(offset, limit int) (*models.MarzbanUsersResponse, error
resp.StatusCode, body)
}
var users models.MarzbanUsersResponse
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
var marzbanResp models.MarzbanUsersResponse
if err := json.NewDecoder(resp.Body).Decode(&marzbanResp); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return &users, nil
users := &models.UsersResponse{
Users: make([]models.User, len(marzbanResp.Users)),
Total: marzbanResp.Total,
}
for i, user := range marzbanResp.Users {
users.Users[i] = models.User{
MarzbanUser: user,
}
}
return users, nil
}

256
source/marzneshin.go Normal file
View file

@ -0,0 +1,256 @@
package source
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"remnawave-migrate/models"
)
type MarzneshinPanel struct {
client *http.Client
baseURL string
authToken string
}
type MarzneshinUser struct {
ID int `json:"id"`
Username string `json:"username"`
ExpireStrategy string `json:"expire_strategy"`
ExpireDate *string `json:"expire_date"`
DataLimit *int64 `json:"data_limit"`
DataLimitResetStrategy string `json:"data_limit_reset_strategy"`
Note *string `json:"note"`
Key string `json:"key"`
Activated bool `json:"activated"`
IsActive bool `json:"is_active"`
Expired bool `json:"expired"`
DataLimitReached bool `json:"data_limit_reached"`
Enabled bool `json:"enabled"`
SubscriptionURL string `json:"subscription_url"`
}
type MarzneshinUsersResponse struct {
Items []MarzneshinUser `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
Pages int `json:"pages"`
}
func NewMarzneshinPanel(baseURL string) *MarzneshinPanel {
return &MarzneshinPanel{
client: &http.Client{},
baseURL: baseURL,
}
}
func (p *MarzneshinPanel) Login(username, password string) error {
data := url.Values{}
data.Set("username", username)
data.Set("password", password)
req, err := http.NewRequest("POST", p.baseURL+"/api/admins/token",
strings.NewReader(data.Encode()))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := p.client.Do(req)
if err != nil {
return fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("login failed: status %d, body: %s", resp.StatusCode, body)
}
var tokenResp struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
p.authToken = tokenResp.AccessToken
return nil
}
func (p *MarzneshinPanel) GetUsers(offset, limit int) (*models.UsersResponse, error) {
page := (offset / limit) + 1
req, err := http.NewRequest("GET",
fmt.Sprintf("%s/api/users?page=%d&size=%d", p.baseURL, page, limit),
nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+p.authToken)
resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("getting users failed: status %d, body: %s",
resp.StatusCode, body)
}
var marzneshinResp MarzneshinUsersResponse
if err := json.NewDecoder(resp.Body).Decode(&marzneshinResp); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
users := &models.UsersResponse{
Users: make([]models.User, len(marzneshinResp.Items)),
Total: marzneshinResp.Total,
}
for i, user := range marzneshinResp.Items {
vlessID, trojanPassword, ssPassword, err := p.fetchUserProxies(user.Username, user.Key)
if err != nil {
return nil, fmt.Errorf("error fetching proxies for user %s: %w", user.Username, err)
}
processedUser := models.ProcessedUser{
Username: user.Username,
VlessID: vlessID,
TrojanPassword: trojanPassword,
ShadowsocksPassword: ssPassword,
SubscriptionHash: user.Key,
DataLimitResetStrategy: strings.ToUpper(user.DataLimitResetStrategy),
Note: getStringValue(user.Note),
}
if user.ExpireDate != nil {
processedUser.Expire = *user.ExpireDate
} else {
farFuture := time.Date(2099, 12, 31, 15, 13, 22, 214000000, time.UTC).Format("2006-01-02T15:04:05.000Z")
processedUser.Expire = farFuture
}
if user.DataLimit != nil {
processedUser.DataLimit = *user.DataLimit
}
if !user.Enabled {
processedUser.Status = "DISABLED"
} else if user.Expired || user.DataLimitReached {
processedUser.Status = "EXPIRED"
} else if user.Activated && user.IsActive {
processedUser.Status = "ACTIVE"
} else {
processedUser.Status = "INACTIVE"
}
users.Users[i] = models.User{
ProcessedUser: processedUser,
}
}
return users, nil
}
func (p *MarzneshinPanel) fetchUserProxies(username, key string) (string, string, string, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/sub/%s/%s", p.baseURL, username, key), nil)
if err != nil {
return "", "", "", fmt.Errorf("creating subscription request: %w", err)
}
resp, err := p.client.Do(req)
if err != nil {
return "", "", "", fmt.Errorf("fetching subscription: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", "", "", fmt.Errorf("subscription request failed: status %d, body: %s",
resp.StatusCode, body)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", fmt.Errorf("reading subscription body: %w", err)
}
decoded, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
return "", "", "", fmt.Errorf("decoding subscription content: %w", err)
}
configs := strings.Split(string(decoded), "\n")
var vlessID, trojanPassword, ssPassword string
for _, config := range configs {
if strings.HasPrefix(config, "vless://") {
vlessID = extractVlessID(config)
} else if strings.HasPrefix(config, "trojan://") {
trojanPassword = extractTrojanPassword(config)
} else if strings.HasPrefix(config, "ss://") {
ssPassword = extractShadowsocksPassword(config)
}
}
return vlessID, trojanPassword, ssPassword, nil
}
func extractVlessID(config string) string {
re := regexp.MustCompile(`vless://([^@]+)@`)
matches := re.FindStringSubmatch(config)
if len(matches) > 1 {
return matches[1]
}
return ""
}
func extractTrojanPassword(config string) string {
re := regexp.MustCompile(`trojan://([^@]+)@`)
matches := re.FindStringSubmatch(config)
if len(matches) > 1 {
return matches[1]
}
return ""
}
func extractShadowsocksPassword(config string) string {
re := regexp.MustCompile(`ss://([^#@]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 1 {
decoded, err := base64.StdEncoding.DecodeString(matches[1])
if err != nil {
return ""
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) == 2 {
return parts[1]
}
}
return ""
}
func getStringValue(s *string) string {
if s == nil {
return ""
}
return *s
}

24
source/source.go Normal file
View file

@ -0,0 +1,24 @@
package source
import (
"fmt"
"remnawave-migrate/models"
)
type SourcePanel interface {
Login(username, password string) error
GetUsers(offset, limit int) (*models.UsersResponse, error)
}
func Factory(panelType, baseURL string) (SourcePanel, error) {
switch panelType {
case "marzban":
return NewMarzbanPanel(baseURL), nil
case "marzneshin":
return NewMarzneshinPanel(baseURL), nil
default:
return nil, fmt.Errorf("unsupported panel type: %s", panelType)
}
}

View file

@ -6,26 +6,20 @@ import (
)
func SanitizeUsername(username string) string {
// Define regex pattern for valid characters
validPattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// Create a new string builder
var sanitized strings.Builder
// Keep only valid characters
for _, char := range username {
if validPattern.MatchString(string(char)) {
sanitized.WriteRune(char)
} else {
// Replace invalid characters with underscore
sanitized.WriteRune('_')
}
}
// Get the sanitized username
result := sanitized.String()
// Ensure minimum length of 6 characters
if len(result) < 6 {
result = result + strings.Repeat("_", 6-len(result))
}

View file

@ -74,7 +74,6 @@ func TestEnsureValidUsername(t *testing.T) {
}
}
// TestUsernameLength ensures the function handles various length scenarios correctly
func TestUsernameLength(t *testing.T) {
tests := []struct {
name string
@ -111,7 +110,6 @@ func TestUsernameLength(t *testing.T) {
}
}
// TestRegexCompliance ensures all characters in the output comply with the regex pattern
func TestRegexCompliance(t *testing.T) {
validPattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
tests := []string{