dae/common/subscription/subscription.go
2024-01-04 17:28:16 +08:00

188 lines
4.6 KiB
Go

/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package subscription
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/config"
"github.com/sirupsen/logrus"
)
type sip008 struct {
Version int `json:"version"`
Servers []sip008Server `json:"servers"`
BytesUsed int64 `json:"bytes_used"`
BytesRemaining int64 `json:"bytes_remaining"`
}
type sip008Server struct {
Id string `json:"id"`
Remarks string `json:"remarks"`
Server string `json:"server"`
ServerPort int `json:"server_port"`
Password string `json:"password"`
Method string `json:"method"`
Plugin string `json:"plugin"`
PluginOpts string `json:"plugin_opts"`
}
func ResolveSubscriptionAsBase64(log *logrus.Logger, b []byte) (nodes []string) {
log.Debugln("Try to resolve as base64")
// base64 decode
raw, e := common.Base64StdDecode(string(b))
if e != nil {
raw, _ = common.Base64UrlDecode(string(b))
}
// Simply check and preprocess.
lines := strings.Split(raw, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
protocol, suffix, _ := strings.Cut(line, "://")
if len(protocol) == 0 || len(suffix) == 0 {
continue
}
nodes = append(nodes, line)
}
return nodes
}
func ResolveSubscriptionAsSIP008(log *logrus.Logger, b []byte) (nodes []string, err error) {
log.Debugln("Try to resolve as sip008")
var sip sip008
err = json.Unmarshal(b, &sip)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal json to sip008")
}
if sip.Version != 1 || sip.Servers == nil {
return nil, fmt.Errorf("does not seems like a standard sip008 subscription")
}
for _, server := range sip.Servers {
u := url.URL{
Scheme: "ss",
User: url.UserPassword(server.Method, server.Password),
Host: net.JoinHostPort(server.Server, strconv.Itoa(server.ServerPort)),
RawQuery: url.Values{"plugin": []string{server.PluginOpts}}.Encode(),
Fragment: server.Remarks,
}
nodes = append(nodes, u.String())
}
return nodes, nil
}
func ResolveFile(u *url.URL, configDir string) (b []byte, err error) {
if u.Host == "" {
return nil, fmt.Errorf("not support absolute path")
}
/// Relative location.
// Make sure path is secure.
path := filepath.Join(configDir, u.Host, u.Path)
if err = common.EnsureFileInSubDir(path, configDir); err != nil {
return nil, err
}
/// Read and resolve.
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
// Check file access.
fi, err := f.Stat()
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("subscription file cannot be a directory: %v", path)
}
if fi.Mode()&0037 > 0 {
return nil, fmt.Errorf("permissions %04o for '%v' are too open; requires the file is NOT writable by the same group and NOT accessible by others; suggest 0640 or 0600", fi.Mode()&0777, path)
}
// Resolve the first line instruction.
fReader := bufio.NewReader(f)
b, err = fReader.Peek(1)
if err != nil {
return nil, err
}
if string(b[0]) == "@" {
// Instruction line. But not support yet.
_, _, err = fReader.ReadLine()
if err != nil {
return nil, err
}
}
b, err = io.ReadAll(fReader)
if err != nil {
return nil, err
}
return bytes.TrimSpace(b), err
}
func ResolveSubscription(log *logrus.Logger, client *http.Client, configDir string, subscription string) (tag string, nodes []string, err error) {
/// Get tag.
tag, subscription = common.GetTagFromLinkLikePlaintext(subscription)
/// Parse url.
u, err := url.Parse(subscription)
if err != nil {
return tag, nil, fmt.Errorf("failed to parse subscription \"%v\": %w", subscription, err)
}
log.Debugf("ResolveSubscription: %v", subscription)
var (
b []byte
req *http.Request
resp *http.Response
)
switch u.Scheme {
case "file":
b, err = ResolveFile(u, configDir)
if err != nil {
return "", nil, err
}
goto resolve
default:
}
req, err = http.NewRequest("GET", subscription, nil)
if err != nil {
return "", nil, err
}
req.Header.Set("User-Agent", fmt.Sprintf("dae/%v (like v2rayA/1.0 WebRequestHelper) (like v2rayN/1.0 WebRequestHelper)", config.Version))
resp, err = client.Do(req)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
b, err = io.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}
resolve:
if nodes, err = ResolveSubscriptionAsSIP008(log, b); err == nil {
return tag, nodes, nil
} else {
log.Debugln(err)
}
return tag, ResolveSubscriptionAsBase64(log, b), nil
}