Compare commits

..

No commits in common. "main" and "v0.0.10" have entirely different histories.

16 changed files with 58 additions and 315 deletions

1
.gitattributes vendored
View File

@ -1 +0,0 @@
* text=auto eol=lf

View File

@ -1,5 +1,7 @@
# sub2clash # sub2clash
> Sing-box 用户?看看另一个项目 [sub2sing-box](https://github.com/nitezs/sub2sing-box)
将订阅链接转换为 Clash、Clash.Meta 配置 将订阅链接转换为 Clash、Clash.Meta 配置
[预览](https://www.nite07.com/sub) [预览](https://www.nite07.com/sub)
@ -17,7 +19,6 @@
- Trojan - Trojan
- Hysteria Clash.Meta - Hysteria Clash.Meta
- Hysteria2 Clash.Meta - Hysteria2 Clash.Meta
- Socks5
## 使用 ## 使用

View File

@ -120,9 +120,6 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
newProxies := make([]model.Proxy, 0, len(proxyList)) newProxies := make([]model.Proxy, 0, len(proxyList))
for i := range proxyList { for i := range proxyList {
key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password
if proxyList[i].Network == "ws" {
key += proxyList[i].WSOpts.Path + proxyList[i].WSOpts.Headers["Host"]
}
if _, exist := proxies[key]; !exist { if _, exist := proxies[key]; !exist {
proxies[key] = &proxyList[i] proxies[key] = &proxyList[i]
newProxies = append(newProxies, proxyList[i]) newProxies = append(newProxies, proxyList[i])

View File

@ -32,41 +32,16 @@ func GenerateLinkHandler(c *gin.Context) {
return return
} }
var hash string hash, err := generateUniqueHash()
var password string if err != nil {
var err error respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
return
if params.CustomID != "" {
// 检查自定义ID是否已存在
exists, err := database.CheckShortLinkHashExists(params.CustomID)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "数据库错误")
return
}
if exists {
respondWithError(c, http.StatusBadRequest, "短链已存在")
return
}
hash = params.CustomID
password = params.Password
} else {
// 自动生成短链ID和密码
hash, err = generateUniqueHash()
if err != nil {
respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
return
}
if params.Password == "" {
password = common.RandomString(8) // 生成8位随机密码
} else {
password = params.Password
}
} }
shortLink := model.ShortLink{ shortLink := model.ShortLink{
Hash: hash, Hash: hash,
Url: params.Url, Url: params.Url,
Password: password, Password: params.Password,
} }
if err := database.SaveShortLink(&shortLink); err != nil { if err := database.SaveShortLink(&shortLink); err != nil {
@ -74,12 +49,10 @@ func GenerateLinkHandler(c *gin.Context) {
return return
} }
// 返回生成的短链ID和密码 if params.Password != "" {
response := map[string]string{ hash += "?password=" + params.Password
"hash": hash,
"password": password,
} }
c.JSON(http.StatusOK, response) c.String(http.StatusOK, hash)
} }
func generateUniqueHash() (string, error) { func generateUniqueHash() (string, error) {
@ -101,27 +74,11 @@ func UpdateLinkHandler(c *gin.Context) {
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error()) respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
return return
} }
// 先获取原有的短链接
existingLink, err := database.FindShortLinkByHash(params.Hash)
if err != nil {
respondWithError(c, http.StatusNotFound, "未找到短链接")
return
}
// 验证密码
if existingLink.Password != params.Password {
respondWithError(c, http.StatusUnauthorized, "密码错误")
return
}
// 更新URL但保持原密码不变
shortLink := model.ShortLink{ shortLink := model.ShortLink{
Hash: params.Hash, Hash: params.Hash,
Url: params.Url, Url: params.Url,
Password: existingLink.Password, // 保持原密码不变 Password: params.Password,
} }
if err := database.SaveShortLink(&shortLink); err != nil { if err := database.SaveShortLink(&shortLink); err != nil {
respondWithError(c, http.StatusInternalServerError, "数据库错误") respondWithError(c, http.StatusInternalServerError, "数据库错误")
return return

View File

@ -79,7 +79,7 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="user-agent">ua标识:</label> <label for="user-agent">ua标识:</label>
<textarea class="form-control" id="user-agent" name="user-agent" <textarea class="form-control" id="user-agent" name="user-agent"
placeholder="用于获取订阅的http请求中的user-agent标识(可选)" rows="3"></textarea> placeholder="用于获取订阅的http请求中的user-agent标识(可选)" rows="1"></textarea>
</div> </div>
<!-- Refresh --> <!-- Refresh -->
<div class="form-check mb-3"> <div class="form-check mb-3">
@ -147,26 +147,23 @@
<div class="form-group mb-5"> <div class="form-group mb-5">
<label for="apiLink">配置链接:</label> <label for="apiLink">配置链接:</label>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input class="form-control bg-light" id="apiLink" type="text" placeholder="链接" readonly style="cursor: not-allowed;" /> <input class="form-control" id="apiLink" type="text" placeholder="链接" />
<button class="btn btn-primary" onclick="copyToClipboard('apiLink',this)" type="button"> <button class="btn btn-primary" onclick="copyToClipboard('apiLink',this)" type="button">
复制链接 复制链接
</button> </button>
</div> </div>
<div class="input-group mb-2"> <div class="input-group">
<input class="form-control" id="customId" type="text" placeholder="短链ID可选" /> <input class="form-control" id="apiShortLink" type="text" placeholder="链接" />
<input class="form-control" id="password" type="text" placeholder="密码(可选)" /> <input class="form-control" id="password" type="text" placeholder="密码" />
<button class="btn btn-primary" onclick="generateShortLink()" type="button"> <button class="btn btn-primary" onclick="generateShortLink()" type="button">
生成短链 生成短链
</button> </button>
<button class="btn btn-primary" onclick="copyToClipboard('apiShortLink',this)" type="button">
复制短链
</button>
</div>
<div class="input-group">
<input class="form-control bg-light" id="apiShortLink" type="text" placeholder="短链接" readonly style="cursor: not-allowed;" />
<button class="btn btn-primary" onclick="updateShortLink()" type="button"> <button class="btn btn-primary" onclick="updateShortLink()" type="button">
更新短链 更新短链
</button> </button>
<button class="btn btn-primary" onclick="copyToClipboard('apiShortLink',this)" type="button">
复制短链
</button>
</div> </div>
</div> </div>

View File

@ -1,15 +1,3 @@
function setInputReadOnly(input, readonly) {
if (readonly) {
input.readOnly = true;
input.classList.add('bg-light');
input.style.cursor = 'not-allowed';
} else {
input.readOnly = false;
input.classList.remove('bg-light');
input.style.cursor = 'auto';
}
}
function clearExistingValues() { function clearExistingValues() {
// 清除简单输入框和复选框的值 // 清除简单输入框和复选框的值
document.getElementById("endpoint").value = "clash"; document.getElementById("endpoint").value = "clash";
@ -24,23 +12,7 @@ function clearExistingValues() {
document.getElementById("remove").value = ""; document.getElementById("remove").value = "";
document.getElementById("apiLink").value = ""; document.getElementById("apiLink").value = "";
document.getElementById("apiShortLink").value = ""; document.getElementById("apiShortLink").value = "";
document.getElementById("password").value = "";
// 恢复短链ID和密码输入框状态
const customIdInput = document.getElementById("customId");
const passwordInput = document.getElementById("password");
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
customIdInput.value = "";
setInputReadOnly(customIdInput, false);
passwordInput.value = "";
setInputReadOnly(passwordInput, false);
// 恢复生成短链按钮状态
generateButton.disabled = false;
generateButton.classList.remove('btn-secondary');
generateButton.classList.add('btn-primary');
document.getElementById("nodeList").checked = false; document.getElementById("nodeList").checked = false;
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组 // 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
@ -97,7 +69,7 @@ function generateURI() {
// 获取订阅user-agent标识 // 获取订阅user-agent标识
const userAgent = document.getElementById("user-agent").value; const userAgent = document.getElementById("user-agent").value;
queryParams.push(`userAgent=${encodeURIComponent(userAgent)}`); queryParams.push(`userAgent=${userAgent}`)
// 获取复选框的值 // 获取复选框的值
const refresh = document.getElementById("refresh").checked; const refresh = document.getElementById("refresh").checked;
@ -216,32 +188,8 @@ async function parseInputURL() {
try { try {
const response = await axios.get("./short?" + q.toString()); const response = await axios.get("./short?" + q.toString());
url = new URL(window.location.href + response.data); url = new URL(window.location.href + response.data);
document.querySelector("#apiShortLink").value = inputURL;
// 回显配置链接 document.querySelector("#password").value = password;
const apiLinkInput = document.querySelector("#apiLink");
apiLinkInput.value = `${window.location.origin}${window.location.pathname}${response.data}`;
setInputReadOnly(apiLinkInput, true);
// 回显短链相关信息
const apiShortLinkInput = document.querySelector("#apiShortLink");
apiShortLinkInput.value = inputURL;
setInputReadOnly(apiShortLinkInput, true);
// 设置短链ID和密码并设置为只读
const customIdInput = document.querySelector("#customId");
const passwordInput = document.querySelector("#password");
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
customIdInput.value = hash;
setInputReadOnly(customIdInput, true);
passwordInput.value = password;
setInputReadOnly(passwordInput, true);
// 禁用生成短链按钮
generateButton.disabled = true;
generateButton.classList.add('btn-secondary');
generateButton.classList.remove('btn-primary');
} catch (error) { } catch (error) {
console.log(error); console.log(error);
alert("获取短链失败,请检查密码!"); alert("获取短链失败,请检查密码!");
@ -307,17 +255,6 @@ async function parseInputURL() {
); );
} }
if (params.has("userAgent")) {
document.getElementById("user-agent").value = decodeURIComponent(
params.get("userAgent")
);
}
if (params.has("ignoreCountryGroup")) {
document.getElementById("igcg").checked =
params.get("ignoreCountryGroup") === "true";
}
if (params.has("replace")) { if (params.has("replace")) {
parseAndFillReplaceParams(decodeURIComponent(params.get("replace"))); parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
} }
@ -491,25 +428,21 @@ function generateURL() {
return; return;
} }
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`; apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
setInputReadOnly(apiLink, true);
} }
function generateShortLink() { function generateShortLink() {
const apiShortLink = document.getElementById("apiShortLink"); const apiShortLink = document.getElementById("apiShortLink");
const password = document.getElementById("password"); const password = document.getElementById("password");
const customId = document.getElementById("customId");
let uri = generateURI(); let uri = generateURI();
if (uri === "") { if (uri === "") {
return; return;
} }
axios axios
.post( .post(
"./short", "./short",
{ {
url: uri, url: uri,
password: password.value.trim(), password: password.value.trim(),
customId: customId.value.trim()
}, },
{ {
headers: { headers: {
@ -518,20 +451,11 @@ function generateShortLink() {
} }
) )
.then((response) => { .then((response) => {
// 设置返回的短链ID和密码 apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
customId.value = response.data.hash;
password.value = response.data.password;
// 生成完整的短链接
const shortLink = `${window.location.origin}${window.location.pathname}s/${response.data.hash}?password=${response.data.password}`;
apiShortLink.value = shortLink;
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
if (error.response && error.response.data) { alert("生成短链失败,请重试!");
alert(error.response.data);
} else {
alert("生成短链失败,请重试!");
}
}); });
} }
@ -544,7 +468,7 @@ function updateShortLink() {
hash = u.pathname.substring(u.pathname.lastIndexOf("/s/") + 3); hash = u.pathname.substring(u.pathname.lastIndexOf("/s/") + 3);
} }
if (password.value.trim() === "") { if (password.value.trim() === "") {
alert("请输入密码进行验证"); alert("请输入密码!");
return; return;
} }
let uri = generateURI(); let uri = generateURI();
@ -566,17 +490,11 @@ function updateShortLink() {
} }
) )
.then((response) => { .then((response) => {
alert(`短链 ${hash} 更新成功!`); alert("更新短链成功!");
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
if (error.response && error.response.status === 401) { alert(error.response.data);
alert("密码错误,请输入正确的原密码!");
} else if (error.response && error.response.data) {
alert(error.response.data);
} else {
alert("更新短链失败,请重试!");
}
}); });
} }

View File

@ -2,7 +2,6 @@ package common
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"time" "time"
@ -29,12 +28,10 @@ func Get(url string, options ...GetOption) (resp *http.Response, err error) {
for _, option := range options { for _, option := range options {
option(&getConfig) option(&getConfig)
} }
var req *http.Request
var get *http.Response
for haveTried < retryTimes { for haveTried < retryTimes {
client := &http.Client{} client := &http.Client{}
//client.Timeout = time.Second * 10 //client.Timeout = time.Second * 10
req, err = http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
haveTried++ haveTried++
time.Sleep(retryDelay) time.Sleep(retryDelay)
@ -43,12 +40,13 @@ func Get(url string, options ...GetOption) (resp *http.Response, err error) {
if getConfig.userAgent != "" { if getConfig.userAgent != "" {
req.Header.Set("User-Agent", getConfig.userAgent) req.Header.Set("User-Agent", getConfig.userAgent)
} }
get, err = client.Do(req) get, err := client.Do(req)
if err != nil { if err != nil {
haveTried++ haveTried++
time.Sleep(retryDelay) time.Sleep(retryDelay)
continue continue
} else { } else {
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize { if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
return nil, errors.New("文件过大") return nil, errors.New("文件过大")
} }
@ -56,5 +54,5 @@ func Get(url string, options ...GetOption) (resp *http.Response, err error) {
} }
} }
return nil, fmt.Errorf("请求失败:%v", err) return nil, err
} }

View File

@ -127,9 +127,6 @@ func ParseProxy(proxies ...string) []model.Proxy {
if strings.HasPrefix(proxy, constant.HysteriaPrefix) { if strings.HasPrefix(proxy, constant.HysteriaPrefix) {
proxyItem, err = parser.ParseHysteria(proxy) proxyItem, err = parser.ParseHysteria(proxy)
} }
if strings.HasPrefix(proxy, constant.SocksPrefix) {
proxyItem, err = parser.ParseSocks(proxy)
}
if err == nil { if err == nil {
result = append(result, proxyItem) result = append(result, proxyItem)
} else { } else {

View File

@ -9,5 +9,4 @@ const (
TrojanPrefix string = "trojan://" TrojanPrefix string = "trojan://"
VLESSPrefix string = "vless://" VLESSPrefix string = "vless://"
VMessPrefix string = "vmess://" VMessPrefix string = "vmess://"
SocksPrefix string = "socks"
) )

View File

@ -14,7 +14,6 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool {
"ssr": true, "ssr": true,
"vmess": true, "vmess": true,
"trojan": true, "trojan": true,
"socks5": true,
} }
} }
if clashType == ClashMeta { if clashType == ClashMeta {
@ -26,7 +25,6 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool {
"vless": true, "vless": true,
"hysteria": true, "hysteria": true,
"hysteria2": true, "hysteria2": true,
"socks5": true,
} }
} }
return nil return nil

View File

@ -10,7 +10,6 @@ type Proxy struct {
Port int `yaml:"port,omitempty"` Port int `yaml:"port,omitempty"`
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Cipher string `yaml:"cipher,omitempty"` Cipher string `yaml:"cipher,omitempty"`
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"` Password string `yaml:"password,omitempty"`
UDP bool `yaml:"udp,omitempty"` UDP bool `yaml:"udp,omitempty"`
UUID string `yaml:"uuid,omitempty"` UUID string `yaml:"uuid,omitempty"`

View File

@ -13,22 +13,7 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) { if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy} return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
} }
if !strings.Contains(proxy, "@") {
s := strings.SplitN(proxy, "#", 2)
d, err := DecodeBase64(strings.TrimPrefix(s[0], "ss://"))
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
}
if len(s) == 2 {
proxy = "ss://" + d + "#" + s[1]
} else {
proxy = "ss://" + d
}
}
link, err := url.Parse(proxy) link, err := url.Parse(proxy)
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
@ -63,29 +48,32 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
} }
} }
method := link.User.Username() user, err := DecodeBase64(link.User.Username())
password, _ := link.User.Password() if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing method and password",
Raw: proxy,
}
}
if password == "" { if user == "" {
user, err := DecodeBase64(method) return model.Proxy{}, &ParseError{
if err == nil { Type: ErrInvalidStruct,
methodAndPass := strings.SplitN(user, ":", 2) Message: "missing method and password",
if len(methodAndPass) == 2 { Raw: proxy,
method = methodAndPass[0]
password = methodAndPass[1]
}
} }
} }
if isLikelyBase64(password) { methodAndPass := strings.SplitN(user, ":", 2)
password, err = DecodeBase64(password) if len(methodAndPass) != 2 {
if err != nil { return model.Proxy{}, &ParseError{
return model.Proxy{}, &ParseError{ Type: ErrInvalidStruct,
Type: ErrInvalidStruct, Message: "missing method and password",
Message: "password decode error", Raw: proxy,
Raw: proxy,
}
} }
} }
method := methodAndPass[0]
password := methodAndPass[1]
remarks := link.Fragment remarks := link.Fragment
if remarks == "" { if remarks == "" {
@ -104,17 +92,3 @@ func ParseShadowsocks(proxy string) (model.Proxy, error) {
return result, nil return result, nil
} }
func isLikelyBase64(s string) bool {
if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") {
s = strings.TrimSuffix(s, "=")
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for _, c := range s {
if !strings.ContainsRune(chars, c) {
return false
}
}
return true
}
return false
}

View File

@ -28,14 +28,7 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
protocol := parts[2] protocol := parts[2]
method := parts[3] method := parts[3]
obfs := parts[4] obfs := parts[4]
password, err := DecodeBase64(parts[5]) password := parts[5]
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Raw: proxy,
Message: err.Error(),
}
}
port, err := ParsePort(parts[1]) port, err := ParsePort(parts[1])
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{

View File

@ -1,79 +0,0 @@
package parser
import (
"fmt"
"github.com/nitezs/sub2clash/constant"
"github.com/nitezs/sub2clash/model"
"net/url"
"strings"
)
func ParseSocks(proxy string) (model.Proxy, error) {
if !strings.HasPrefix(proxy, constant.SocksPrefix) {
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
}
link, err := url.Parse(proxy)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
}
server := link.Hostname()
if server == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server host",
Raw: proxy,
}
}
portStr := link.Port()
if portStr == "" {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "missing server port",
Raw: proxy,
}
}
port, err := ParsePort(portStr)
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidPort,
Raw: portStr,
}
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
encodeStr := link.User.Username()
var username, password string
if encodeStr != "" {
decodeStr, err := DecodeBase64(encodeStr)
splitStr := strings.Split(decodeStr, ":")
if err != nil {
return model.Proxy{}, &ParseError{
Type: ErrInvalidStruct,
Message: "url parse error",
Raw: proxy,
}
}
username = splitStr[0]
if len(splitStr) == 2 {
password = splitStr[1]
}
}
return model.Proxy{
Type: "socks5",
Name: remarks,
Server: server,
Port: port,
Username: username,
Password: password,
}, nil
}

View File

@ -104,11 +104,6 @@ func ParseVless(proxy string) (model.Proxy, error) {
} }
if _type == "http" { if _type == "http" {
result.HTTPOpts = model.HTTPOptions{}
result.HTTPOpts.Headers = map[string][]string{}
result.HTTPOpts.Path = strings.Split(path, ",")
hosts, err := url.QueryUnescape(host) hosts, err := url.QueryUnescape(host)
if err != nil { if err != nil {
return model.Proxy{}, &ParseError{ return model.Proxy{}, &ParseError{
@ -118,9 +113,10 @@ func ParseVless(proxy string) (model.Proxy, error) {
} }
} }
result.Network = "http" result.Network = "http"
if hosts != "" { result.HTTPOpts = model.HTTPOptions{
result.HTTPOpts.Headers["host"] = strings.Split(host, ",") Headers: map[string][]string{"Host": strings.Split(hosts, ",")},
} }
} }
return result, nil return result, nil

View File

@ -3,7 +3,6 @@ package validator
type ShortLinkGenValidator struct { type ShortLinkGenValidator struct {
Url string `form:"url" binding:"required"` Url string `form:"url" binding:"required"`
Password string `form:"password"` Password string `form:"password"`
CustomID string `form:"customId"`
} }
type GetUrlValidator struct { type GetUrlValidator struct {