mirror of
https://github.com/v2fly/v2ray-core.git
synced 2024-12-21 09:36:34 -05:00
Add subscription manager
This commit is contained in:
parent
b91354901c
commit
cc77e90254
@ -2,6 +2,7 @@ package command
|
||||
|
||||
import (
|
||||
observatory "github.com/v2fly/v2ray-core/v5/app/observatory"
|
||||
_ "github.com/v2fly/v2ray-core/v5/common/protoext"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
@ -154,39 +155,43 @@ var file_app_observatory_command_command_proto_rawDesc = []byte{
|
||||
0x79, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
|
||||
0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x22, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63,
|
||||
0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74,
|
||||
0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x1c, 0x61, 0x70, 0x70,
|
||||
0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x63, 0x6f, 0x6e,
|
||||
0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x18, 0x47, 0x65, 0x74,
|
||||
0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x20, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x74, 0x2f, 0x65, 0x78, 0x74,
|
||||
0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x61,
|
||||
0x70, 0x70, 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x63,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x18, 0x47,
|
||||
0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x61, 0x67, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x54, 0x61, 0x67, 0x22, 0x62, 0x0a, 0x19, 0x47, 0x65, 0x74,
|
||||
0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x03, 0x54, 0x61, 0x67, 0x22, 0x62, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x75,
|
||||
0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63,
|
||||
0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74,
|
||||
0x6f, 0x72, 0x79, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52,
|
||||
0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x28, 0x0a,
|
||||
0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x3a, 0x1e, 0x82, 0xb5, 0x18, 0x1a, 0x0a, 0x0b, 0x67,
|
||||
0x72, 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0b, 0x6f, 0x62, 0x73, 0x65,
|
||||
0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x32, 0xa9, 0x01, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65,
|
||||
0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x92,
|
||||
0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74,
|
||||
0x61, 0x74, 0x75, 0x73, 0x12, 0x3c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
|
||||
0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72,
|
||||
0x79, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x08, 0x0a, 0x06, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xa9, 0x01, 0x0a, 0x12, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76,
|
||||
0x61, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x92, 0x01, 0x0a,
|
||||
0x11, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x12, 0x3c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e,
|
||||
0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74,
|
||||
0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f,
|
||||
0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x3d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e,
|
||||
0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x00, 0x42, 0x87, 0x01, 0x0a, 0x26, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
|
||||
0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61,
|
||||
0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x36,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79,
|
||||
0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x61,
|
||||
0x70, 0x70, 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x22, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43,
|
||||
0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74,
|
||||
0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x22, 0x00, 0x42, 0x87, 0x01, 0x0a, 0x26, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61,
|
||||
0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6f, 0x62, 0x73, 0x65, 0x72,
|
||||
0x76, 0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01,
|
||||
0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66,
|
||||
0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35,
|
||||
0x2f, 0x61, 0x70, 0x70, 0x2f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x6f, 0x72, 0x79,
|
||||
0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x22, 0x56, 0x32, 0x52, 0x61, 0x79,
|
||||
0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76,
|
||||
0x61, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -6,6 +6,7 @@ option go_package = "github.com/v2fly/v2ray-core/v5/app/observatory/command";
|
||||
option java_package = "com.v2ray.core.app.observatory.command";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protoext/extensions.proto";
|
||||
import "app/observatory/config.proto";
|
||||
|
||||
message GetOutboundStatusRequest {
|
||||
@ -22,4 +23,7 @@ service ObservatoryService {
|
||||
}
|
||||
|
||||
|
||||
message Config {}
|
||||
message Config {
|
||||
option (v2ray.core.common.protoext.message_opt).type = "grpcservice";
|
||||
option (v2ray.core.common.protoext.message_opt).short_name = "observatory";
|
||||
}
|
@ -73,6 +73,7 @@ func (o *Observer) background() {
|
||||
|
||||
o.updateStatus(outbounds)
|
||||
|
||||
slept := false
|
||||
for _, v := range outbounds {
|
||||
result := o.probe(v)
|
||||
o.updateStatusForResult(v, &result)
|
||||
@ -84,6 +85,14 @@ func (o *Observer) background() {
|
||||
sleepTime = time.Duration(o.config.ProbeInterval)
|
||||
}
|
||||
time.Sleep(sleepTime)
|
||||
slept = true
|
||||
}
|
||||
if !slept {
|
||||
sleepTime := time.Second * 10
|
||||
if o.config.ProbeInterval != 0 {
|
||||
sleepTime = time.Duration(o.config.ProbeInterval)
|
||||
}
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ func (s *handlerServer) AddOutbound(ctx context.Context, request *AddOutboundReq
|
||||
}
|
||||
|
||||
func (s *handlerServer) RemoveOutbound(ctx context.Context, request *RemoveOutboundRequest) (*RemoveOutboundResponse, error) {
|
||||
return &RemoveOutboundResponse{}, s.ohm.RemoveHandler(ctx, request.Tag)
|
||||
return &RemoveOutboundResponse{}, core.RemoveOutboundHandler(s.s, request.Tag)
|
||||
}
|
||||
|
||||
func (s *handlerServer) AlterOutbound(ctx context.Context, request *AlterOutboundRequest) (*AlterOutboundResponse, error) {
|
||||
|
@ -317,5 +317,11 @@ func (h *Handler) Start() error {
|
||||
// Close implements common.Closable.
|
||||
func (h *Handler) Close() error {
|
||||
common.Close(h.mux)
|
||||
|
||||
if closableProxy, ok := h.proxy.(common.Closable); ok {
|
||||
if err := closableProxy.Close(); err != nil {
|
||||
return newError("unable to close proxy").Base(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/v2fly/v2ray-core/v5/app/proxyman"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
"github.com/v2fly/v2ray-core/v5/common/errors"
|
||||
"github.com/v2fly/v2ray-core/v5/common/session"
|
||||
"github.com/v2fly/v2ray-core/v5/features/outbound"
|
||||
)
|
||||
|
||||
@ -131,12 +132,18 @@ func (m *Manager) RemoveHandler(ctx context.Context, tag string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
delete(m.taggedHandler, tag)
|
||||
if m.defaultHandler != nil && m.defaultHandler.Tag() == tag {
|
||||
m.defaultHandler = nil
|
||||
if handler, found := m.taggedHandler[tag]; found {
|
||||
if err := handler.Close(); err != nil {
|
||||
newError("failed to close handler ", tag).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx))
|
||||
}
|
||||
delete(m.taggedHandler, tag)
|
||||
if m.defaultHandler != nil && m.defaultHandler.Tag() == tag {
|
||||
m.defaultHandler = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return common.ErrNoClue
|
||||
}
|
||||
|
||||
// Select implements outbound.HandlerSelector.
|
||||
|
31
app/subscription/config.proto
Normal file
31
app/subscription/config.proto
Normal file
@ -0,0 +1,31 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package v2ray.core.app.subscription;
|
||||
|
||||
option csharp_namespace = "V2Ray.Core.App.Subscription";
|
||||
option go_package = "github.com/v2fly/v2ray-core/v5/app/subscription";
|
||||
option java_package = "com.v2ray.core.app.subscription";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protoext/extensions.proto";
|
||||
|
||||
message ImportSource {
|
||||
string name = 1;
|
||||
string url = 2;
|
||||
string tag_prefix = 3;
|
||||
|
||||
string import_network_tag = 4;
|
||||
|
||||
uint64 default_expire_seconds = 5;
|
||||
}
|
||||
|
||||
// Config is the settings for Subscription Manager.
|
||||
message Config {
|
||||
option (v2ray.core.common.protoext.message_opt).type = "service";
|
||||
option (v2ray.core.common.protoext.message_opt).short_name = "subscription";
|
||||
|
||||
repeated ImportSource imports = 1;
|
||||
|
||||
bytes nonnative_converter_overlay = 2;
|
||||
string nonnative_converter_overlay_file = 96002 [(v2ray.core.common.protoext.field_opt).convert_time_read_file_into = "nonnative_converter_overlay"];
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package base64urlline
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
46
app/subscription/containers/base64urlline/parser.go
Normal file
46
app/subscription/containers/base64urlline/parser.go
Normal file
@ -0,0 +1,46 @@
|
||||
package base64urlline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
)
|
||||
|
||||
func newBase64URLLineParser() containers.SubscriptionContainerDocumentParser {
|
||||
return &parser{}
|
||||
}
|
||||
|
||||
type parser struct{}
|
||||
|
||||
func (p parser) ParseSubscriptionContainerDocument(rawConfig []byte) (*containers.Container, error) {
|
||||
result := &containers.Container{}
|
||||
result.Kind = "Base64URLLine"
|
||||
result.Metadata = make(map[string]string)
|
||||
|
||||
bodyDecoder := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(rawConfig))
|
||||
decoded, err := io.ReadAll(bodyDecoder)
|
||||
if err != nil {
|
||||
return nil, newError("failed to decode base64url body base64").Base(err)
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewReader(decoded))
|
||||
|
||||
const maxCapacity int = 1024 * 256
|
||||
buf := make([]byte, maxCapacity)
|
||||
scanner.Buffer(buf, maxCapacity)
|
||||
|
||||
for scanner.Scan() {
|
||||
result.ServerSpecs = append(result.ServerSpecs, containers.UnparsedServerConf{
|
||||
KindHint: "URL",
|
||||
Content: scanner.Bytes(),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(containers.RegisterParser("Base64URLLine", newBase64URLLineParser()))
|
||||
}
|
28
app/subscription/containers/containers.go
Normal file
28
app/subscription/containers/containers.go
Normal file
@ -0,0 +1,28 @@
|
||||
package containers
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
type UnparsedServerConf struct {
|
||||
KindHint string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Kind string
|
||||
Metadata map[string]string
|
||||
ServerSpecs []UnparsedServerConf
|
||||
}
|
||||
|
||||
type SubscriptionContainerDocumentParser interface {
|
||||
ParseSubscriptionContainerDocument(rawConfig []byte) (*Container, error)
|
||||
}
|
||||
|
||||
var knownParsers = make(map[string]SubscriptionContainerDocumentParser)
|
||||
|
||||
func RegisterParser(kind string, parser SubscriptionContainerDocumentParser) error {
|
||||
if _, found := knownParsers[kind]; found {
|
||||
return newError("parser already registered for kind ", kind)
|
||||
}
|
||||
knownParsers[kind] = parser
|
||||
return nil
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package jsonfieldarray
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
@ -0,0 +1,3 @@
|
||||
package jsonified
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
@ -0,0 +1,36 @@
|
||||
package jsonified
|
||||
|
||||
import (
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers/jsonfieldarray"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
jsonConf "github.com/v2fly/v2ray-core/v5/infra/conf/json"
|
||||
)
|
||||
|
||||
func newJsonifiedYamlParser() containers.SubscriptionContainerDocumentParser {
|
||||
return &jsonifiedYAMLParser{}
|
||||
}
|
||||
|
||||
type jsonifiedYAMLParser struct{}
|
||||
|
||||
func (j jsonifiedYAMLParser) ParseSubscriptionContainerDocument(rawConfig []byte) (*containers.Container, error) {
|
||||
parser := jsonfieldarray.NewJSONFieldArrayParser()
|
||||
jsonified, err := jsonConf.FromYAML(rawConfig)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse as yaml").Base(err)
|
||||
}
|
||||
container, err := parser.ParseSubscriptionContainerDocument(jsonified)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse as jsonfieldarray").Base(err)
|
||||
}
|
||||
container.Kind = "Yaml2Json+" + container.Kind
|
||||
|
||||
for _, value := range container.ServerSpecs {
|
||||
value.KindHint = "Yaml2Json+" + value.KindHint
|
||||
}
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(containers.RegisterParser("Yaml2Json", newJsonifiedYamlParser()))
|
||||
}
|
68
app/subscription/containers/jsonfieldarray/parser.go
Normal file
68
app/subscription/containers/jsonfieldarray/parser.go
Normal file
@ -0,0 +1,68 @@
|
||||
package jsonfieldarray
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
)
|
||||
|
||||
// NewJSONFieldArrayParser internal api
|
||||
func NewJSONFieldArrayParser() containers.SubscriptionContainerDocumentParser {
|
||||
return newJSONFieldArrayParser()
|
||||
}
|
||||
|
||||
func newJSONFieldArrayParser() containers.SubscriptionContainerDocumentParser {
|
||||
return &parser{}
|
||||
}
|
||||
|
||||
type parser struct{}
|
||||
|
||||
type jsonDocument map[string]json.RawMessage
|
||||
|
||||
func (p parser) ParseSubscriptionContainerDocument(rawConfig []byte) (*containers.Container, error) {
|
||||
result := &containers.Container{}
|
||||
result.Kind = "JsonFieldArray"
|
||||
result.Metadata = make(map[string]string)
|
||||
|
||||
var doc jsonDocument
|
||||
if err := json.Unmarshal(rawConfig, &doc); err != nil {
|
||||
return nil, newError("failed to parse as json").Base(err)
|
||||
}
|
||||
|
||||
for key, value := range doc {
|
||||
switch value[0] {
|
||||
case '[':
|
||||
parsedArray, err := p.parseArray(value, "JsonFieldArray+"+key)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse as json array").Base(err)
|
||||
}
|
||||
result.ServerSpecs = append(result.ServerSpecs, parsedArray...)
|
||||
case '{':
|
||||
fallthrough
|
||||
default:
|
||||
result.Metadata[key] = string(value)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p parser) parseArray(rawConfig []byte, kindHint string) ([]containers.UnparsedServerConf, error) {
|
||||
var result []json.RawMessage
|
||||
if err := json.Unmarshal(rawConfig, &result); err != nil {
|
||||
return nil, newError("failed to parse as json array").Base(err)
|
||||
}
|
||||
var ret []containers.UnparsedServerConf
|
||||
for _, value := range result {
|
||||
ret = append(ret, containers.UnparsedServerConf{
|
||||
KindHint: kindHint,
|
||||
Content: []byte(value),
|
||||
})
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(containers.RegisterParser("JsonFieldArray", newJSONFieldArrayParser()))
|
||||
}
|
20
app/subscription/containers/tryall.go
Normal file
20
app/subscription/containers/tryall.go
Normal file
@ -0,0 +1,20 @@
|
||||
package containers
|
||||
|
||||
func TryAllParsers(rawConfig []byte, prioritizedParser string) (*Container, error) {
|
||||
if prioritizedParser != "" {
|
||||
if parser, found := knownParsers[prioritizedParser]; found {
|
||||
container, err := parser.ParseSubscriptionContainerDocument(rawConfig)
|
||||
if err == nil {
|
||||
return container, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, parser := range knownParsers {
|
||||
container, err := parser.ParseSubscriptionContainerDocument(rawConfig)
|
||||
if err == nil {
|
||||
return container, nil
|
||||
}
|
||||
}
|
||||
return nil, newError("no parser found for config")
|
||||
}
|
32
app/subscription/documentfetcher/fetcher.go
Normal file
32
app/subscription/documentfetcher/fetcher.go
Normal file
@ -0,0 +1,32 @@
|
||||
package documentfetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
type FetcherOptions interface{}
|
||||
|
||||
type Fetcher interface {
|
||||
DownloadDocument(ctx context.Context, source *subscription.ImportSource, opts ...FetcherOptions) ([]byte, error)
|
||||
}
|
||||
|
||||
var knownFetcher = make(map[string]Fetcher)
|
||||
|
||||
func RegisterFetcher(name string, fetcher Fetcher) error {
|
||||
if _, found := knownFetcher[name]; found {
|
||||
return newError("fetcher ", name, " already registered")
|
||||
}
|
||||
knownFetcher[name] = fetcher
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetFetcher(name string) (Fetcher, error) {
|
||||
if fetcher, found := knownFetcher[name]; found {
|
||||
return fetcher, nil
|
||||
}
|
||||
return nil, newError("fetcher ", name, " not found")
|
||||
}
|
60
app/subscription/documentfetcher/httpfetcher/http.go
Normal file
60
app/subscription/documentfetcher/httpfetcher/http.go
Normal file
@ -0,0 +1,60 @@
|
||||
package httpfetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
gonet "net"
|
||||
"net/http"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
"github.com/v2fly/v2ray-core/v5/common/net"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/documentfetcher"
|
||||
"github.com/v2fly/v2ray-core/v5/common/environment"
|
||||
"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
func newHTTPFetcher() *httpFetcher {
|
||||
return &httpFetcher{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(documentfetcher.RegisterFetcher("http", newHTTPFetcher()))
|
||||
}
|
||||
|
||||
type httpFetcher struct{}
|
||||
|
||||
func (h *httpFetcher) DownloadDocument(ctx context.Context, source *subscription.ImportSource, opts ...documentfetcher.FetcherOptions) ([]byte, error) {
|
||||
instanceNetwork := envctx.EnvironmentFromContext(ctx).(environment.InstanceNetworkCapabilitySet)
|
||||
outboundDialer := instanceNetwork.OutboundDialer()
|
||||
var httpRoundTripper http.RoundTripper //nolint: gosimple
|
||||
httpRoundTripper = &http.Transport{
|
||||
DialContext: func(ctx_ context.Context, network string, addr string) (gonet.Conn, error) {
|
||||
dest, err := net.ParseDestination(network + ":" + addr)
|
||||
if err != nil {
|
||||
return nil, newError("unable to parse destination")
|
||||
}
|
||||
return outboundDialer(ctx, dest, source.ImportNetworkTag)
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequest("GET", source.Url, nil)
|
||||
if err != nil {
|
||||
return nil, newError("unable to generate request").Base(err)
|
||||
}
|
||||
resp, err := httpRoundTripper.RoundTrip(request)
|
||||
if err != nil {
|
||||
return nil, newError("unable to send request").Base(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, newError("unexpected http status ", resp.StatusCode, "=", resp.Status)
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, newError("unable to read response").Base(err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
9
app/subscription/entries/entries.go
Normal file
9
app/subscription/entries/entries.go
Normal file
@ -0,0 +1,9 @@
|
||||
package entries
|
||||
|
||||
import "github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
type Converter interface {
|
||||
ConvertToAbstractServerConfig(rawConfig []byte, kindHint string) (*specs.SubscriptionServerConfig, error)
|
||||
}
|
51
app/subscription/entries/nonnative/converter.go
Normal file
51
app/subscription/entries/nonnative/converter.go
Normal file
@ -0,0 +1,51 @@
|
||||
package nonnative
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative/nonnativeifce"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries/outbound"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
)
|
||||
|
||||
type nonNativeConverter struct {
|
||||
matcher *DefMatcher
|
||||
}
|
||||
|
||||
func (n *nonNativeConverter) ConvertToAbstractServerConfig(rawConfig []byte, kindHint string) (*specs.SubscriptionServerConfig, error) {
|
||||
nonNativeLink := ExtractAllValuesFromBytes(rawConfig)
|
||||
nonNativeLink.Values["_kind"] = kindHint
|
||||
result, err := n.matcher.ExecuteAll(nonNativeLink)
|
||||
if err != nil {
|
||||
return nil, newError("failed to find working converting template").Base(err)
|
||||
}
|
||||
outboundParser := outbound.NewOutboundEntriesParser()
|
||||
outboundEntries, err := outboundParser.ConvertToAbstractServerConfig(result, "")
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse template output as outbound entries").Base(err)
|
||||
}
|
||||
return outboundEntries, nil
|
||||
}
|
||||
|
||||
func NewNonNativeConverter(fs fs.FS) (entries.Converter, error) {
|
||||
matcher := NewDefMatcher()
|
||||
if fs == nil {
|
||||
err := matcher.LoadEmbeddedDefinitions()
|
||||
if err != nil {
|
||||
return nil, newError("failed to load embedded definitions").Base(err)
|
||||
}
|
||||
} else {
|
||||
err := matcher.LoadDefinitions(fs)
|
||||
if err != nil {
|
||||
return nil, newError("failed to load provided definitions").Base(err)
|
||||
}
|
||||
}
|
||||
return &nonNativeConverter{matcher: matcher}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(entries.RegisterConverter("nonnative", common.Must2(NewNonNativeConverter(nil)).(entries.Converter)))
|
||||
nonnativeifce.NewNonNativeConverterConstructor = NewNonNativeConverter
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{{if assertExists . "root_!kind" | not}} Unknown environment {{end}}
|
||||
{{if assertIsOneOf . "root_!kind" "json" | not}} This template only works for json input. {{end}}
|
||||
|
||||
{{ $methodName := tryGet . "root_!json_method_!unquoted" "root_!json_protocol_!unquoted" "root_!json_cipher_!unquoted"}}
|
||||
{{if assertValueIsOneOf $methodName "chacha20-ietf-poly1305" "chacha20-poly1305" "aes-128-gcm" "aes-256-gcm" | not}}
|
||||
This template only works for ss. {{end}}
|
||||
|
||||
{{ $server_address := tryGet . "root_!json_server" "root_!json_address" "root_!json_endpoint"}}
|
||||
{{ $server_port := tryGet . "root_!json_port" "root_!json_server_port" "root_!json_endpoint"}}
|
||||
{{if $server_address | splitAndGetAfterNth ":" 0 | len | gt 1}}
|
||||
{{ $server_addressport_unquoted := tryGet . "root_!json_endpoint_!unquoted"}}
|
||||
{{ $server_port = $server_addressport_unquoted | splitAndGetAfterNth ":" -1}}
|
||||
|
||||
{{ $server_portWithSep := printf ":%v" $server_port}}
|
||||
{{ $server_address = $server_addressport_unquoted | stringCutSuffix $server_portWithSep | jsonEncode}}
|
||||
{{end}}
|
||||
|
||||
{{ $name_annotation := tryGet . "root_!json_name_!unquoted" "root_!json_id_!unquoted" "root_!json_tag_!unquoted" "root_!json_remarks_!unquoted" "<default>"}}
|
||||
|
||||
{{$password := tryGet . "root_!json_password" "root_!json_psk"}}
|
||||
|
||||
{
|
||||
"protocol": "shadowsocks",
|
||||
"settings": {
|
||||
"address": {{$server_address}},
|
||||
"port": {{$server_port}},
|
||||
"method": {{$methodName | jsonEncode}},
|
||||
"password": {{$password}}
|
||||
},
|
||||
"metadata":{
|
||||
|
||||
"TagName": {{print $name_annotation "_" $server_address | jsonEncode}}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
{{if assertExists . "root_!kind" | not}} Unknown environment {{end}}
|
||||
{{if assertIsOneOf . "root_!kind" "json" | not}} This template only works for json input. {{end}}
|
||||
|
||||
{{ $methodName := tryGet . "root_!json_method_!unquoted" "root_!json_protocol_!unquoted"}}
|
||||
{{if assertValueIsOneOf $methodName "2022-blake3-aes-128-gcm" "2022-blake3-aes-256-gcm" | not}}
|
||||
This template only works for ss2022. {{end}}
|
||||
|
||||
{{ $server_address := tryGet . "root_!json_server" "root_!json_address" "root_!json_endpoint"}}
|
||||
{{ $server_port := tryGet . "root_!json_port" "root_!json_server_port" "root_!json_endpoint"}}
|
||||
{{if $server_address | splitAndGetAfterNth ":" 0 | len | gt 1}}
|
||||
{{ $server_addressport_unquoted := tryGet . "root_!json_endpoint_!unquoted"}}
|
||||
{{ $server_port = $server_addressport_unquoted | splitAndGetAfterNth ":" -1}}
|
||||
|
||||
{{ $server_portWithSep := printf ":%v" $server_port}}
|
||||
{{ $server_address = $server_addressport_unquoted | stringCutSuffix $server_portWithSep | jsonEncode}}
|
||||
{{end}}
|
||||
|
||||
{{ $name_annotation := tryGet . "root_!json_name_!unquoted" "root_!json_id_!unquoted" "root_!json_tag_!unquoted" "root_!json_remarks_!unquoted" "<default>"}}
|
||||
|
||||
{{ $psk := tryGet . "root_!json_password_!unquoted" "root_!json_psk_!unquoted"}}
|
||||
{{ $ipsk_encoded := "" }}
|
||||
{{if $psk | splitAndGetAfterNth ":" 0 | len | ne 1}}
|
||||
{{ $origpsk := $psk }}
|
||||
{{ $psk = $psk | splitAndGetNth ":" -1 }}
|
||||
{{ $pskWithSep := printf ":%v" $psk}}
|
||||
{{ $ipsk_encoded = $origpsk | stringCutSuffix $pskWithSep | splitAndGetAfterNth ":" 0 | jsonEncode}}
|
||||
{{else}}
|
||||
{{$ipsk_encoded = tryGet . "root_!json_iPSKs" "<default>"}}
|
||||
{{end}}
|
||||
|
||||
|
||||
{
|
||||
"protocol": "shadowsocks2022",
|
||||
"settings": {
|
||||
"address": {{$server_address}},
|
||||
"port": {{$server_port}},
|
||||
"method": {{$methodName | jsonEncode}},
|
||||
"psk": {{$psk | jsonEncode}}
|
||||
{{if $ipsk_encoded|len|ne 0}}
|
||||
,
|
||||
"ipsk": {{$ipsk_encoded}}
|
||||
{{end}}
|
||||
},
|
||||
|
||||
"metadata":{
|
||||
|
||||
"TagName": {{print $name_annotation "_" $server_address | jsonEncode}}
|
||||
|
||||
}
|
||||
|
||||
}
|
66
app/subscription/entries/nonnative/definitions/vmess.jsont
Normal file
66
app/subscription/entries/nonnative/definitions/vmess.jsont
Normal file
@ -0,0 +1,66 @@
|
||||
{{if assertExists . "root_!kind" | not}} Unknown environment {{end}}
|
||||
{{ $protocol_name := tryGet . "root_!link_protocol" "root_!json_type_!unquoted"}}
|
||||
{{if assertValueIsOneOf $protocol_name "vmess" | not}} This template will only handle vmess link {{end}}
|
||||
|
||||
{{ $server_address := tryGet . "root_!link_host_!base64_!json_add" "root_!json_server"}}
|
||||
{{ $server_uuid := tryGet . "root_!link_host_!base64_!json_id" "root_!json_uuid"}}
|
||||
{{ $server_port := tryGet . "root_!link_host_!base64_!json_port_!unquoted" "root_!link_host_!base64_!json_port" "root_!json_port_!unquoted" "root_!json_port"}}
|
||||
|
||||
{{ $transport_type := tryGet . "root_!link_host_!base64_!json_net_!unquoted" "root_!json_network_!unquoted" "<default>"}}
|
||||
{{ $transport_type = $transport_type | unalias "tcp" ""}}
|
||||
|
||||
{{ $name_annotation := tryGet . "root_!link_host_!base64_!json_ps_!unquoted" "root_!json_name_!unquoted" "<default>"}}
|
||||
|
||||
{{if assertValueIsOneOf $transport_type "tcp" "kcp" "ws" "h2" "quic" "grpc"| not }}
|
||||
unknown transport type {{end}}
|
||||
|
||||
{{$transport_grpc_service_name := ""}}
|
||||
{{ if $transport_type | eq "grpc"}}
|
||||
{{ $transport_grpc_service_name = tryGet . "root_!link_host_!base64_!json_path" "<default>"}}
|
||||
{{end}}
|
||||
|
||||
{{$transport_ws_path := ""}}
|
||||
{{ if $transport_type | eq "ws"}}
|
||||
{{ $transport_ws_path = tryGet . "root_!link_host_!base64_!json_path" "root_!json_ws-opts_!json_path" "<default>"}}
|
||||
{{end}}
|
||||
|
||||
{{ $security_type := tryGet . "root_!link_host_!base64_!json_tls_!unquoted" "root_!json_tls" "<default>"}}
|
||||
{{ $security_type = $security_type | unalias "none" "" "false"}}
|
||||
|
||||
{{if assertValueIsOneOf $security_type "tls" "utls" "none"| not }}
|
||||
unknown security type {{end}}
|
||||
|
||||
{{ $security_tlsmmon_sni := tryGet . "root_!link_host_!base64_!json_sni" "<default>"}}
|
||||
{{ $security_tlsmmon_sni = $security_tlsmmon_sni | unalias $server_address ""}}
|
||||
|
||||
{
|
||||
"protocol": "vmess",
|
||||
"settings":{
|
||||
"address":{{$server_address}},
|
||||
"port":{{$server_port}},
|
||||
"uuid":{{$server_uuid}}
|
||||
},
|
||||
"streamSettings":{
|
||||
"transport":{{$transport_type|jsonEncode}},
|
||||
"security":{{$security_type|jsonEncode}},
|
||||
"transportSettings":{
|
||||
{{ if $transport_type | eq "grpc"}}
|
||||
"serviceName":{{$transport_grpc_service_name}}
|
||||
{{end}}
|
||||
{{ if $transport_type | eq "ws"}}
|
||||
"path":{{$transport_ws_path}}
|
||||
{{end}}
|
||||
},
|
||||
|
||||
"securitySettings":{
|
||||
{{ if $security_type | eq "tls"}}
|
||||
"serverName":{{$security_tlsmmon_sni}}
|
||||
{{end}}
|
||||
}
|
||||
},
|
||||
"metadata":{
|
||||
|
||||
"TagName": {{print $name_annotation "_" $server_address | jsonEncode}}
|
||||
|
||||
}
|
||||
}
|
191
app/subscription/entries/nonnative/matchdef.go
Normal file
191
app/subscription/entries/nonnative/matchdef.go
Normal file
@ -0,0 +1,191 @@
|
||||
package nonnative
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed definitions/*
|
||||
var embeddedDefinitions embed.FS
|
||||
|
||||
func NewDefMatcher() *DefMatcher {
|
||||
d := &DefMatcher{}
|
||||
d.init()
|
||||
return d
|
||||
}
|
||||
|
||||
type DefMatcher struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
type ExecutionEnvironment struct {
|
||||
link AbstractNonNativeLink
|
||||
}
|
||||
|
||||
func (d *DefMatcher) createFuncMap() template.FuncMap {
|
||||
return map[string]any{
|
||||
"assertExists": func(env *ExecutionEnvironment, names ...string) (bool, error) {
|
||||
link := env.link
|
||||
for _, v := range names {
|
||||
_, ok := link.Values[v]
|
||||
if !ok {
|
||||
return false, newError("failed assertExists of ", v)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
"assertIsOneOf": func(env *ExecutionEnvironment, name string, values ...string) (bool, error) {
|
||||
link := env.link
|
||||
actualValue, ok := link.Values[name]
|
||||
if !ok {
|
||||
return false, newError("failed assertIs of non-exist ", name)
|
||||
}
|
||||
found := false
|
||||
for _, currentValue := range values {
|
||||
if currentValue == actualValue {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false, newError("failed assertIsOneOf name = ", actualValue, "is not one of ", values)
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
"assertValueIsOneOf": func(value string, values ...string) (bool, error) {
|
||||
actualValue := value
|
||||
found := false
|
||||
for _, currentValue := range values {
|
||||
if currentValue == actualValue {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false, newError("failed assertIsOneOf name = ", actualValue, "is not one of ", values)
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
"tryGet": func(env *ExecutionEnvironment, names ...string) (string, error) {
|
||||
link := env.link
|
||||
for _, currentName := range names {
|
||||
value, ok := link.Values[currentName]
|
||||
if ok {
|
||||
return value, nil
|
||||
} else if currentName == "<default>" {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return "", newError("failed tryGet exists none of ", names)
|
||||
},
|
||||
"splitAndGetNth": func(sep string, n int, content string) (string, error) {
|
||||
result := strings.Split(content, sep)
|
||||
if n > len(result)-1 {
|
||||
return "", newError("failed splitAndGetNth exists too short content:", content, "n = ", n, "sep =", sep)
|
||||
}
|
||||
if n < 0 {
|
||||
n = len(result) + n
|
||||
if n < 0 {
|
||||
return "", newError("failed splitAndGetNth exists too short content:", content, "n = ", n, "sep =", sep)
|
||||
}
|
||||
}
|
||||
return result[n], nil
|
||||
},
|
||||
"splitAndGetAfterNth": func(sep string, n int, content string) ([]string, error) {
|
||||
result := strings.Split(content, sep)
|
||||
if n < 0 {
|
||||
n = len(result) + n
|
||||
}
|
||||
if n > len(result)-1 {
|
||||
return []string{}, newError("failed splitAndGetNth exists too short content:", content)
|
||||
}
|
||||
return result[n:], nil
|
||||
},
|
||||
"splitAndGetBeforeNth": func(sep string, n int, content string) ([]string, error) {
|
||||
result := strings.Split(content, sep)
|
||||
if n < 0 {
|
||||
n = len(result) + n
|
||||
}
|
||||
if n > len(result)-1 {
|
||||
return []string{}, newError("failed splitAndGetNth exists too short content:", content)
|
||||
}
|
||||
return result[:n], nil
|
||||
},
|
||||
"jsonEncode": func(content any) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := json.NewEncoder(buf).Encode(content)
|
||||
if err != nil {
|
||||
return "", newError("unable to jsonQuote ", content).Base(err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
},
|
||||
"stringCutSuffix": func(suffix, content string) (string, error) {
|
||||
remaining, found := strings.CutSuffix(content, suffix)
|
||||
if !found {
|
||||
return "", newError("suffix not found in content =", suffix, " suffix =", suffix)
|
||||
}
|
||||
return remaining, nil
|
||||
},
|
||||
"unalias": func(standardName string, names ...string) (string, error) {
|
||||
if len(names) == 0 {
|
||||
return "", newError("no input value specified")
|
||||
}
|
||||
actualInput := names[len(names)-1]
|
||||
alias := names[:len(names)-1]
|
||||
for _, v := range alias {
|
||||
if v == actualInput {
|
||||
return standardName, nil
|
||||
}
|
||||
}
|
||||
return actualInput, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefMatcher) init() {
|
||||
d.templates = template.New("root").Funcs(d.createFuncMap())
|
||||
}
|
||||
|
||||
func (d *DefMatcher) LoadEmbeddedDefinitions() error {
|
||||
return d.LoadDefinitions(embeddedDefinitions)
|
||||
}
|
||||
|
||||
func (d *DefMatcher) LoadDefinitions(fs fs.FS) error {
|
||||
var err error
|
||||
d.templates, err = d.templates.ParseFS(fs, "definitions/*.jsont")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DefMatcher) ExecuteNamed(link AbstractNonNativeLink, name string) ([]byte, error) {
|
||||
outputBuffer := bytes.NewBuffer(nil)
|
||||
env := &ExecutionEnvironment{link: link}
|
||||
err := d.templates.ExecuteTemplate(outputBuffer, name, env)
|
||||
if err != nil {
|
||||
return nil, newError("failed to execute template").Base(err)
|
||||
}
|
||||
return outputBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (d *DefMatcher) ExecuteAll(link AbstractNonNativeLink) ([]byte, error) {
|
||||
outputBuffer := bytes.NewBuffer(nil)
|
||||
for _, loadedTemplates := range d.templates.Templates() {
|
||||
env := &ExecutionEnvironment{link: link}
|
||||
err := loadedTemplates.Execute(outputBuffer, env)
|
||||
if err != nil {
|
||||
outputBuffer.Reset()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if outputBuffer.Len() == 0 {
|
||||
return nil, newError("failed to find a working template")
|
||||
}
|
||||
return outputBuffer.Bytes(), nil
|
||||
}
|
108
app/subscription/entries/nonnative/nonnative.go
Normal file
108
app/subscription/entries/nonnative/nonnative.go
Normal file
@ -0,0 +1,108 @@
|
||||
package nonnative
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
func ExtractAllValuesFromBytes(bytes []byte) AbstractNonNativeLink {
|
||||
link := AbstractNonNativeLink{}
|
||||
link.fromBytes(bytes)
|
||||
return link
|
||||
}
|
||||
|
||||
type jsonDocument map[string]json.RawMessage
|
||||
|
||||
type AbstractNonNativeLink struct {
|
||||
Values map[string]string
|
||||
}
|
||||
|
||||
func (a *AbstractNonNativeLink) fromBytes(bytes []byte) {
|
||||
a.Values = make(map[string]string)
|
||||
content := string(bytes)
|
||||
content = strings.Trim(content, " \n\t\r")
|
||||
a.extractValue(content, "root")
|
||||
}
|
||||
|
||||
func (a *AbstractNonNativeLink) extractValue(content, prefix string) {
|
||||
{
|
||||
// check if the content is a link
|
||||
match, err := regexp.Match("[a-zA-Z0-9]+:((\\/\\/)|\\?)", []byte(content))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if match {
|
||||
// if so, parse as link
|
||||
parsedURL, err := url.Parse(content)
|
||||
// if process is successful, then continue to parse every element of the link
|
||||
if err == nil {
|
||||
a.Values[prefix+"_!kind"] = "link"
|
||||
a.extractLink(parsedURL, prefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// check if it is base64
|
||||
content = strings.Trim(content, "=")
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(content)
|
||||
if err == nil {
|
||||
a.Values[prefix+"_!kind"] = "base64"
|
||||
a.extractValue(string(decoded), prefix+"_!base64")
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
// check if it is base64url
|
||||
content = strings.Trim(content, "=")
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(content)
|
||||
if err == nil {
|
||||
a.Values[prefix+"_!kind"] = "base64url"
|
||||
a.extractValue(string(decoded), prefix+"_!base64")
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
// check if it is json
|
||||
var doc jsonDocument
|
||||
if err := json.Unmarshal([]byte(content), &doc); err == nil {
|
||||
a.Values[prefix+"_!kind"] = "json"
|
||||
a.extractJSON(&doc, prefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AbstractNonNativeLink) extractLink(content *url.URL, prefix string) {
|
||||
a.Values[prefix+"_!link"] = content.String()
|
||||
a.Values[prefix+"_!link_protocol"] = content.Scheme
|
||||
a.Values[prefix+"_!link_host"] = content.Host
|
||||
a.extractValue(content.Host, prefix+"_!link_host")
|
||||
a.Values[prefix+"_!link_path"] = content.Path
|
||||
a.Values[prefix+"_!link_query"] = content.RawQuery
|
||||
a.Values[prefix+"_!link_fragment"] = content.Fragment
|
||||
a.Values[prefix+"_!link_userinfo"] = content.User.String()
|
||||
a.Values[prefix+"_!link_opaque"] = content.Opaque
|
||||
}
|
||||
|
||||
func (a *AbstractNonNativeLink) extractJSON(content *jsonDocument, prefix string) {
|
||||
for key, value := range *content {
|
||||
switch value[0] {
|
||||
case '{':
|
||||
a.extractValue(string(value), prefix+"_!json_"+key)
|
||||
case '"':
|
||||
var unquoted string
|
||||
if err := json.Unmarshal(value, &unquoted); err == nil {
|
||||
a.Values[prefix+"_!json_"+key+"_!unquoted"] = unquoted
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
a.Values[prefix+"_!json_"+key] = string(value)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package nonnativeifce
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries"
|
||||
)
|
||||
|
||||
type NonNativeConverterConstructorT func(fs fs.FS) (entries.Converter, error)
|
||||
|
||||
var NewNonNativeConverterConstructor NonNativeConverterConstructorT
|
33
app/subscription/entries/outbound/outbound.go
Normal file
33
app/subscription/entries/outbound/outbound.go
Normal file
@ -0,0 +1,33 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
// NewOutboundEntriesParser internal api
|
||||
func NewOutboundEntriesParser() entries.Converter {
|
||||
return newOutboundEntriesParser()
|
||||
}
|
||||
|
||||
func newOutboundEntriesParser() entries.Converter {
|
||||
return &outboundEntriesParser{}
|
||||
}
|
||||
|
||||
type outboundEntriesParser struct{}
|
||||
|
||||
func (o *outboundEntriesParser) ConvertToAbstractServerConfig(rawConfig []byte, kindHint string) (*specs.SubscriptionServerConfig, error) {
|
||||
parser := specs.NewOutboundParser()
|
||||
outbound, err := parser.ParseOutboundConfig(rawConfig)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse outbound config").Base(err).AtWarning()
|
||||
}
|
||||
return parser.ToSubscriptionServerConfig(outbound)
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(entries.RegisterConverter("outbound", newOutboundEntriesParser()))
|
||||
}
|
54
app/subscription/entries/register.go
Normal file
54
app/subscription/entries/register.go
Normal file
@ -0,0 +1,54 @@
|
||||
package entries
|
||||
|
||||
import "github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
|
||||
type ConverterRegistry struct {
|
||||
knownConverters map[string]Converter
|
||||
parent *ConverterRegistry
|
||||
}
|
||||
|
||||
var globalConverterRegistry = &ConverterRegistry{knownConverters: map[string]Converter{}}
|
||||
|
||||
func RegisterConverter(kind string, converter Converter) error {
|
||||
return globalConverterRegistry.RegisterConverter(kind, converter)
|
||||
}
|
||||
|
||||
func GetOverlayConverterRegistry() *ConverterRegistry {
|
||||
return globalConverterRegistry.GetOverlayConverterRegistry()
|
||||
}
|
||||
|
||||
func (c *ConverterRegistry) RegisterConverter(kind string, converter Converter) error {
|
||||
if _, found := c.knownConverters[kind]; found {
|
||||
return newError("converter already registered for kind ", kind)
|
||||
}
|
||||
c.knownConverters[kind] = converter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConverterRegistry) TryAllConverters(rawConfig []byte, prioritizedConverter, kindHint string) (*specs.SubscriptionServerConfig, error) {
|
||||
if prioritizedConverter != "" {
|
||||
if converter, found := c.knownConverters[prioritizedConverter]; found {
|
||||
serverConfig, err := converter.ConvertToAbstractServerConfig(rawConfig, kindHint)
|
||||
if err == nil {
|
||||
return serverConfig, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, converter := range c.knownConverters {
|
||||
serverConfig, err := converter.ConvertToAbstractServerConfig(rawConfig, kindHint)
|
||||
if err == nil {
|
||||
return serverConfig, nil
|
||||
}
|
||||
}
|
||||
if c.parent != nil {
|
||||
if serverConfig, err := c.parent.TryAllConverters(rawConfig, prioritizedConverter, kindHint); err == nil {
|
||||
return serverConfig, nil
|
||||
}
|
||||
}
|
||||
return nil, newError("no converter found for config")
|
||||
}
|
||||
|
||||
func (c *ConverterRegistry) GetOverlayConverterRegistry() *ConverterRegistry {
|
||||
return &ConverterRegistry{knownConverters: map[string]Converter{}, parent: c}
|
||||
}
|
30
app/subscription/specs/abstract_spec.proto
Normal file
30
app/subscription/specs/abstract_spec.proto
Normal file
@ -0,0 +1,30 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package v2ray.core.app.subscription.specs;
|
||||
|
||||
option csharp_namespace = "V2Ray.Core.App.Subscription.Specs";
|
||||
option go_package = "github.com/v2fly/v2ray-core/v5/app/subscription/specs";
|
||||
option java_package = "com.v2ray.core.app.subscription.specs";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
message ServerConfiguration{
|
||||
string protocol = 1;
|
||||
google.protobuf.Any protocol_settings = 2;
|
||||
string transport = 3;
|
||||
google.protobuf.Any transport_settings = 4;
|
||||
string security = 5;
|
||||
google.protobuf.Any security_settings = 6;
|
||||
}
|
||||
|
||||
message SubscriptionServerConfig{
|
||||
string id = 1;
|
||||
map<string, string> metadata = 2;
|
||||
ServerConfiguration configuration = 3;
|
||||
}
|
||||
|
||||
message SubscriptionDocument {
|
||||
map<string, string> metadata = 2;
|
||||
repeated SubscriptionServerConfig server = 3;
|
||||
}
|
90
app/subscription/specs/outbound_parser.go
Normal file
90
app/subscription/specs/outbound_parser.go
Normal file
@ -0,0 +1,90 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/common/registry"
|
||||
"github.com/v2fly/v2ray-core/v5/common/serial"
|
||||
)
|
||||
|
||||
func NewOutboundParser() *OutboundParser {
|
||||
return &OutboundParser{}
|
||||
}
|
||||
|
||||
type OutboundParser struct{}
|
||||
|
||||
func (p *OutboundParser) ParseOutboundConfig(rawConfig []byte) (*OutboundConfig, error) {
|
||||
skeleton := &OutboundConfig{}
|
||||
decoder := json.NewDecoder(bytes.NewReader(rawConfig))
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(skeleton)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse outbound config skeleton").Base(err)
|
||||
}
|
||||
return skeleton, nil
|
||||
}
|
||||
|
||||
func (p *OutboundParser) toAbstractServerSpec(config *OutboundConfig) (*ServerConfiguration, error) {
|
||||
serverConfig := &ServerConfiguration{}
|
||||
serverConfig.Protocol = config.Protocol
|
||||
{
|
||||
protocolSettings, err := loadHeterogeneousConfigFromRawJSONRestricted("outbound", config.Protocol, config.Settings)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse protocol settings").Base(err)
|
||||
}
|
||||
serverConfig.ProtocolSettings = serial.ToTypedMessage(protocolSettings)
|
||||
}
|
||||
|
||||
if config.StreamSetting != nil {
|
||||
if config.StreamSetting.Transport == "" {
|
||||
config.StreamSetting.Transport = "tcp"
|
||||
}
|
||||
if config.StreamSetting.Security == "" {
|
||||
config.StreamSetting.Security = "none"
|
||||
}
|
||||
{
|
||||
serverConfig.Transport = config.StreamSetting.Transport
|
||||
transportSettings, err := loadHeterogeneousConfigFromRawJSONRestricted(
|
||||
"transport", config.StreamSetting.Transport, config.StreamSetting.TransportSettings)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse transport settings").Base(err)
|
||||
}
|
||||
serverConfig.TransportSettings = serial.ToTypedMessage(transportSettings)
|
||||
}
|
||||
{
|
||||
securitySettings, err := loadHeterogeneousConfigFromRawJSONRestricted(
|
||||
"security", config.StreamSetting.Security, config.StreamSetting.SecuritySettings)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse security settings").Base(err)
|
||||
}
|
||||
|
||||
serverConfig.SecuritySettings = serial.ToTypedMessage(securitySettings)
|
||||
serverConfig.Security = serial.V2Type(serverConfig.SecuritySettings)
|
||||
}
|
||||
}
|
||||
return serverConfig, nil
|
||||
}
|
||||
|
||||
func (p *OutboundParser) ToSubscriptionServerConfig(config *OutboundConfig) (*SubscriptionServerConfig, error) {
|
||||
serverSpec, err := p.toAbstractServerSpec(config)
|
||||
if err != nil {
|
||||
return nil, newError("unable to parse server specification")
|
||||
}
|
||||
return &SubscriptionServerConfig{
|
||||
Configuration: serverSpec,
|
||||
Metadata: config.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadHeterogeneousConfigFromRawJSONRestricted(interfaceType, name string, rawJSON json.RawMessage) (proto.Message, error) {
|
||||
ctx := context.TODO()
|
||||
ctx = registry.CreateRestrictedModeContext(ctx)
|
||||
if len(rawJSON) == 0 {
|
||||
rawJSON = []byte("{}")
|
||||
}
|
||||
return registry.LoadImplementationByAlias(ctx, interfaceType, name, []byte(rawJSON))
|
||||
}
|
19
app/subscription/specs/skeleton.go
Normal file
19
app/subscription/specs/skeleton.go
Normal file
@ -0,0 +1,19 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type OutboundConfig struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Settings json.RawMessage `json:"settings"`
|
||||
StreamSetting *StreamConfig `json:"streamSettings"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
type StreamConfig struct {
|
||||
Transport string `json:"transport"`
|
||||
TransportSettings json.RawMessage `json:"transportSettings"`
|
||||
Security string `json:"security"`
|
||||
SecuritySettings json.RawMessage `json:"securitySettings"`
|
||||
}
|
3
app/subscription/specs/specs.go
Normal file
3
app/subscription/specs/specs.go
Normal file
@ -0,0 +1,3 @@
|
||||
package specs
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
3
app/subscription/subscription.go
Normal file
3
app/subscription/subscription.go
Normal file
@ -0,0 +1,3 @@
|
||||
package subscription
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
8
app/subscription/subscriptionmanager/delta.go
Normal file
8
app/subscription/subscriptionmanager/delta.go
Normal file
@ -0,0 +1,8 @@
|
||||
package subscriptionmanager
|
||||
|
||||
type changedDocument struct {
|
||||
removed []string
|
||||
added []string
|
||||
modified []string
|
||||
unchanged []string
|
||||
}
|
7
app/subscription/subscriptionmanager/known_metadata.go
Normal file
7
app/subscription/subscriptionmanager/known_metadata.go
Normal file
@ -0,0 +1,7 @@
|
||||
package subscriptionmanager
|
||||
|
||||
const (
|
||||
ServerMetadataID = "ID"
|
||||
ServerMetadataTagName = "TagName"
|
||||
ServerMetadataFullyQualifiedName = "FullyQualifiedName"
|
||||
)
|
103
app/subscription/subscriptionmanager/manager.go
Normal file
103
app/subscription/subscriptionmanager/manager.go
Normal file
@ -0,0 +1,103 @@
|
||||
package subscriptionmanager
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
core "github.com/v2fly/v2ray-core/v5"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative/nonnativeifce"
|
||||
"github.com/v2fly/v2ray-core/v5/common"
|
||||
"github.com/v2fly/v2ray-core/v5/common/task"
|
||||
"github.com/v2fly/v2ray-core/v5/features/extension"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
|
||||
|
||||
type SubscriptionManagerImpl struct {
|
||||
config *subscription.Config
|
||||
ctx context.Context
|
||||
|
||||
s *core.Instance
|
||||
converter *entries.ConverterRegistry
|
||||
|
||||
trackedSubscriptions map[string]*trackedSubscription
|
||||
|
||||
refreshTask *task.Periodic
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) Type() interface{} {
|
||||
return extension.SubscriptionManagerType()
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) housekeeping() error {
|
||||
for subscriptionName := range s.trackedSubscriptions {
|
||||
if err := s.checkupSubscription(subscriptionName); err != nil {
|
||||
newError("failed to checkup subscription: ", err).AtWarning().WriteToLog()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) Start() error {
|
||||
if err := s.refreshTask.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) Close() error {
|
||||
if err := s.refreshTask.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) init() error {
|
||||
s.refreshTask = &task.Periodic{
|
||||
Interval: time.Duration(60) * time.Second,
|
||||
Execute: s.housekeeping,
|
||||
}
|
||||
s.trackedSubscriptions = make(map[string]*trackedSubscription)
|
||||
s.converter = entries.GetOverlayConverterRegistry()
|
||||
if s.config.NonnativeConverterOverlay != nil {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(s.config.NonnativeConverterOverlay), int64(len(s.config.NonnativeConverterOverlay)))
|
||||
if err != nil {
|
||||
return newError("failed to read nonnative converter overlay: ", err)
|
||||
}
|
||||
converter, err := nonnativeifce.NewNonNativeConverterConstructor(zipReader)
|
||||
if err != nil {
|
||||
return newError("failed to construct nonnative converter: ", err)
|
||||
}
|
||||
if err := s.converter.RegisterConverter("user_nonnative", converter); err != nil {
|
||||
return newError("failed to register user nonnative converter: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.config.Imports {
|
||||
tracked, err := newTrackedSubscription(v)
|
||||
if err != nil {
|
||||
return newError("failed to init subscription ", v.Name, ": ", err)
|
||||
}
|
||||
s.trackedSubscriptions[v.Name] = tracked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSubscriptionManager(ctx context.Context, config *subscription.Config) (*SubscriptionManagerImpl, error) {
|
||||
instance := core.MustFromContext(ctx)
|
||||
impl := &SubscriptionManagerImpl{ctx: ctx, s: instance, config: config}
|
||||
if err := impl.init(); err != nil {
|
||||
return nil, newError("failed to init subscription manager: ", err)
|
||||
}
|
||||
return impl, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*subscription.Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return NewSubscriptionManager(ctx, config.(*subscription.Config))
|
||||
}))
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package subscriptionmanager
|
||||
|
||||
import (
|
||||
core "github.com/v2fly/v2ray-core/v5"
|
||||
"github.com/v2fly/v2ray-core/v5/app/proxyman"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
"github.com/v2fly/v2ray-core/v5/common/serial"
|
||||
"github.com/v2fly/v2ray-core/v5/transport/internet"
|
||||
)
|
||||
|
||||
func (s *SubscriptionManagerImpl) materialize(subscriptionName, tagName string, serverSpec *specs.SubscriptionServerConfig) (*core.OutboundHandlerConfig, error) {
|
||||
outboundConf, err := s.getOutboundTemplateForSubscriptionName(subscriptionName)
|
||||
if err != nil {
|
||||
return nil, newError("failed to get outbound template for subscription name: ", err)
|
||||
}
|
||||
|
||||
senderSettingsIfcd, err := serial.GetInstanceOf(outboundConf.SenderSettings)
|
||||
if err != nil {
|
||||
return nil, newError("failed to get sender settings: ", err)
|
||||
}
|
||||
senderSettings := senderSettingsIfcd.(*proxyman.SenderConfig)
|
||||
|
||||
if serverSpec.Configuration.Transport != "" {
|
||||
senderSettings.StreamSettings.ProtocolName = serverSpec.Configuration.Transport
|
||||
senderSettings.StreamSettings.TransportSettings = append(senderSettings.StreamSettings.TransportSettings,
|
||||
&internet.TransportConfig{ProtocolName: serverSpec.Configuration.Transport, Settings: serverSpec.Configuration.TransportSettings})
|
||||
}
|
||||
|
||||
if serverSpec.Configuration.Security != "" {
|
||||
senderSettings.StreamSettings.SecurityType = serverSpec.Configuration.Security
|
||||
senderSettings.StreamSettings.SecuritySettings = append(senderSettings.StreamSettings.SecuritySettings,
|
||||
serverSpec.Configuration.SecuritySettings)
|
||||
}
|
||||
|
||||
outboundConf.SenderSettings = serial.ToTypedMessage(senderSettings)
|
||||
|
||||
outboundConf.ProxySettings = serverSpec.Configuration.ProtocolSettings
|
||||
|
||||
outboundConf.Tag = tagName
|
||||
|
||||
return outboundConf, nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) getOutboundTemplateForSubscriptionName(subscriptionName string) (*core.OutboundHandlerConfig, error) { //nolint: unparam
|
||||
senderSetting := &proxyman.SenderConfig{
|
||||
DomainStrategy: proxyman.SenderConfig_AS_IS, StreamSettings: &internet.StreamConfig{},
|
||||
}
|
||||
|
||||
return &core.OutboundHandlerConfig{SenderSettings: serial.ToTypedMessage(senderSetting)}, nil
|
||||
}
|
121
app/subscription/subscriptionmanager/subdocapplier.go
Normal file
121
app/subscription/subscriptionmanager/subdocapplier.go
Normal file
@ -0,0 +1,121 @@
|
||||
package subscriptionmanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
core "github.com/v2fly/v2ray-core/v5"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
)
|
||||
|
||||
func (s *SubscriptionManagerImpl) applySubscriptionTo(name string, document *specs.SubscriptionDocument) error {
|
||||
var trackedSub *trackedSubscription
|
||||
if trackedSubFound, found := s.trackedSubscriptions[name]; !found {
|
||||
return newError("not found")
|
||||
} else {
|
||||
trackedSub = trackedSubFound
|
||||
}
|
||||
|
||||
delta, err := trackedSub.diff(document)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nameToServerConfig := make(map[string]*specs.SubscriptionServerConfig)
|
||||
for _, server := range document.Server {
|
||||
nameToServerConfig[server.Id] = server
|
||||
}
|
||||
|
||||
for _, serverName := range delta.removed {
|
||||
if err := s.removeManagedServer(name, serverName); err != nil {
|
||||
newError("failed to remove managed server: ", err).AtWarning().WriteToLog()
|
||||
continue
|
||||
}
|
||||
trackedSub.recordRemovedServer(serverName)
|
||||
}
|
||||
|
||||
for _, serverName := range delta.modified {
|
||||
serverConfig := nameToServerConfig[serverName]
|
||||
if err := s.updateManagedServer(name, serverName, serverConfig); err != nil {
|
||||
newError("failed to update managed server: ", err).AtWarning().WriteToLog()
|
||||
continue
|
||||
}
|
||||
trackedSub.recordUpdatedServer(serverName, serverConfig.Metadata[ServerMetadataTagName], serverConfig)
|
||||
}
|
||||
|
||||
for _, serverName := range delta.added {
|
||||
serverConfig := nameToServerConfig[serverName]
|
||||
if err := s.addManagedServer(name, serverName, serverConfig); err != nil {
|
||||
newError("failed to add managed server: ", err).AtWarning().WriteToLog()
|
||||
continue
|
||||
}
|
||||
trackedSub.recordUpdatedServer(serverName, serverConfig.Metadata[ServerMetadataTagName], serverConfig)
|
||||
}
|
||||
|
||||
newError("finished applying subscription, ", name, "; ", fmt.Sprintf(
|
||||
"%v updated, %v added, %v removed, %v unchanged",
|
||||
len(delta.modified), len(delta.added), len(delta.removed), len(delta.unchanged))).AtInfo().WriteToLog()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) removeManagedServer(subscriptionName, serverName string) error {
|
||||
var trackedSub *trackedSubscription
|
||||
if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found {
|
||||
return newError("not found")
|
||||
} else {
|
||||
trackedSub = trackedSubFound
|
||||
}
|
||||
|
||||
var trackedServer *materializedServer
|
||||
if trackedServerFound, err := trackedSub.getCurrentServer(serverName); err != nil {
|
||||
return err
|
||||
} else {
|
||||
trackedServer = trackedServerFound
|
||||
}
|
||||
|
||||
tagName := fmt.Sprintf("%s_%s", trackedSub.importSource.TagPrefix, trackedServer.tagPostfix)
|
||||
|
||||
if err := core.RemoveOutboundHandler(s.s, tagName); err != nil {
|
||||
return newError("failed to remove handler: ", err)
|
||||
}
|
||||
trackedSub.recordRemovedServer(serverName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) addManagedServer(subscriptionName, serverName string,
|
||||
serverSpec *specs.SubscriptionServerConfig,
|
||||
) error {
|
||||
var trackedSub *trackedSubscription
|
||||
if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found {
|
||||
return newError("not found")
|
||||
} else {
|
||||
trackedSub = trackedSubFound
|
||||
}
|
||||
tagPostfix := serverSpec.Metadata[ServerMetadataTagName]
|
||||
tagName := fmt.Sprintf("%s_%s", trackedSub.importSource.TagPrefix, tagPostfix)
|
||||
|
||||
materialized, err := s.materialize(subscriptionName, tagName, serverSpec)
|
||||
if err != nil {
|
||||
return newError("failed to materialize server: ", err)
|
||||
}
|
||||
|
||||
if err := core.AddOutboundHandler(s.s, materialized); err != nil {
|
||||
return newError("failed to add handler: ", err)
|
||||
}
|
||||
|
||||
trackedSub.recordUpdatedServer(serverName, tagPostfix, serverSpec)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) updateManagedServer(subscriptionName, serverName string,
|
||||
serverSpec *specs.SubscriptionServerConfig,
|
||||
) error {
|
||||
if err := s.removeManagedServer(subscriptionName, serverName); err != nil {
|
||||
return newError("failed to update managed server: ", err).AtWarning()
|
||||
}
|
||||
if err := s.addManagedServer(subscriptionName, serverName, serverSpec); err != nil {
|
||||
return newError("failed to update managed server : ", err).AtWarning()
|
||||
}
|
||||
return nil
|
||||
}
|
26
app/subscription/subscriptionmanager/subdocchecker.go
Normal file
26
app/subscription/subscriptionmanager/subdocchecker.go
Normal file
@ -0,0 +1,26 @@
|
||||
package subscriptionmanager
|
||||
|
||||
import "time"
|
||||
|
||||
func (s *SubscriptionManagerImpl) checkupSubscription(subscriptionName string) error {
|
||||
var trackedSub *trackedSubscription
|
||||
if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found {
|
||||
return newError("not found")
|
||||
} else {
|
||||
trackedSub = trackedSubFound
|
||||
}
|
||||
|
||||
shouldUpdate := false
|
||||
|
||||
if trackedSub.currentDocumentExpireTime.Before(time.Now()) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if shouldUpdate {
|
||||
if err := s.updateSubscription(subscriptionName); err != nil {
|
||||
return newError("failed to update subscription: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
114
app/subscription/subscriptionmanager/subdocupdater.go
Normal file
114
app/subscription/subscriptionmanager/subdocupdater.go
Normal file
@ -0,0 +1,114 @@
|
||||
package subscriptionmanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/documentfetcher"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
)
|
||||
|
||||
func (s *SubscriptionManagerImpl) updateSubscription(subscriptionName string) error {
|
||||
var trackedSub *trackedSubscription
|
||||
if trackedSubFound, found := s.trackedSubscriptions[subscriptionName]; !found {
|
||||
return newError("not found")
|
||||
} else {
|
||||
trackedSub = trackedSubFound
|
||||
}
|
||||
importSource := trackedSub.importSource
|
||||
|
||||
docFetcher, err := documentfetcher.GetFetcher("http")
|
||||
if err != nil {
|
||||
return newError("failed to get fetcher: ", err)
|
||||
}
|
||||
|
||||
downloadedDocument, err := docFetcher.DownloadDocument(s.ctx, importSource)
|
||||
if err != nil {
|
||||
return newError("failed to download document: ", err)
|
||||
}
|
||||
|
||||
trackedSub.originalDocument = downloadedDocument
|
||||
|
||||
container, err := containers.TryAllParsers(trackedSub.originalDocument, "")
|
||||
if err != nil {
|
||||
return newError("failed to parse document: ", err)
|
||||
}
|
||||
|
||||
trackedSub.originalContainer = container
|
||||
|
||||
parsedDocument := &specs.SubscriptionDocument{}
|
||||
parsedDocument.Metadata = container.Metadata
|
||||
|
||||
trackedSub.originalServerConfig = make(map[string]*originalServerConfig)
|
||||
|
||||
for _, server := range trackedSub.originalContainer.ServerSpecs {
|
||||
documentHash := sha3.Sum256(server.Content)
|
||||
serverConfigHashName := fmt.Sprintf("%x", documentHash)
|
||||
parsed, err := s.converter.TryAllConverters(server.Content, "outbound", server.KindHint)
|
||||
if err != nil {
|
||||
trackedSub.originalServerConfig[serverConfigHashName] = &originalServerConfig{data: server.Content}
|
||||
continue
|
||||
}
|
||||
s.polyfillServerConfig(parsed, serverConfigHashName)
|
||||
parsedDocument.Server = append(parsedDocument.Server, parsed)
|
||||
trackedSub.originalServerConfig[parsed.Id] = &originalServerConfig{data: server.Content}
|
||||
}
|
||||
newError("new subscription document fetched and parsed from ", subscriptionName).AtInfo().WriteToLog()
|
||||
if err := s.applySubscriptionTo(subscriptionName, parsedDocument); err != nil {
|
||||
return newError("failed to apply subscription: ", err)
|
||||
}
|
||||
trackedSub.currentDocument = parsedDocument
|
||||
trackedSub.currentDocumentExpireTime = time.Now().Add(time.Second * time.Duration(importSource.DefaultExpireSeconds))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) polyfillServerConfig(document *specs.SubscriptionServerConfig, hash string) {
|
||||
document.Id = hash
|
||||
|
||||
if document.Metadata == nil {
|
||||
document.Metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
if id, ok := document.Metadata[ServerMetadataID]; !ok || id == "" {
|
||||
document.Metadata[ServerMetadataID] = document.Id
|
||||
} else {
|
||||
document.Id = document.Metadata[ServerMetadataID]
|
||||
}
|
||||
|
||||
if fqn, ok := document.Metadata[ServerMetadataFullyQualifiedName]; !ok || fqn == "" {
|
||||
document.Metadata[ServerMetadataFullyQualifiedName] = hash
|
||||
}
|
||||
|
||||
if tagName, ok := document.Metadata[ServerMetadataTagName]; !ok || tagName == "" {
|
||||
document.Metadata[ServerMetadataTagName] = document.Metadata[ServerMetadataID]
|
||||
}
|
||||
document.Metadata[ServerMetadataTagName] = s.restrictTagName(document.Metadata[ServerMetadataTagName])
|
||||
}
|
||||
|
||||
func (s *SubscriptionManagerImpl) restrictTagName(tagName string) string {
|
||||
newTagName := &strings.Builder{}
|
||||
somethingRemoved := false
|
||||
for _, c := range tagName {
|
||||
if (unicode.IsLetter(c) || unicode.IsNumber(c)) && c < 128 {
|
||||
newTagName.WriteRune(c)
|
||||
} else {
|
||||
somethingRemoved = true
|
||||
}
|
||||
}
|
||||
newTagNameString := newTagName.String()
|
||||
if len(newTagNameString) > 24 {
|
||||
newTagNameString = newTagNameString[:15]
|
||||
somethingRemoved = true
|
||||
}
|
||||
if somethingRemoved {
|
||||
hashedTagName := sha3.Sum256([]byte(tagName))
|
||||
hashedTagNameString := fmt.Sprintf("%x", hashedTagName)
|
||||
newTagNameString = newTagNameString + "_" + hashedTagNameString[:8]
|
||||
}
|
||||
return newTagNameString
|
||||
}
|
78
app/subscription/subscriptionmanager/tracked_subscription.go
Normal file
78
app/subscription/subscriptionmanager/tracked_subscription.go
Normal file
@ -0,0 +1,78 @@
|
||||
package subscriptionmanager
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers"
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/specs"
|
||||
)
|
||||
|
||||
func newTrackedSubscription(importSource *subscription.ImportSource) (*trackedSubscription, error) { //nolint: unparam
|
||||
return &trackedSubscription{importSource: importSource, materialized: map[string]*materializedServer{}}, nil
|
||||
}
|
||||
|
||||
type trackedSubscription struct {
|
||||
importSource *subscription.ImportSource
|
||||
|
||||
currentDocumentExpireTime time.Time
|
||||
currentDocument *specs.SubscriptionDocument
|
||||
|
||||
materialized map[string]*materializedServer
|
||||
|
||||
originalDocument []byte
|
||||
originalContainer *containers.Container
|
||||
originalServerConfig map[string]*originalServerConfig
|
||||
}
|
||||
|
||||
type originalServerConfig struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (s *trackedSubscription) diff(newDocument *specs.SubscriptionDocument) (changedDocument, error) { //nolint: unparam
|
||||
delta := changedDocument{}
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, server := range newDocument.Server {
|
||||
if currentMaterialized, found := s.materialized[server.Id]; found {
|
||||
if currentMaterialized.serverConfig.Metadata[ServerMetadataFullyQualifiedName] == server.Metadata[ServerMetadataFullyQualifiedName] {
|
||||
delta.unchanged = append(delta.unchanged, server.Id)
|
||||
} else {
|
||||
delta.modified = append(delta.modified, server.Id)
|
||||
}
|
||||
seen[server.Id] = true
|
||||
} else {
|
||||
delta.added = append(delta.added, server.Id)
|
||||
}
|
||||
}
|
||||
|
||||
for name := range s.materialized {
|
||||
if _, ok := seen[name]; !ok {
|
||||
delta.removed = append(delta.removed, name)
|
||||
}
|
||||
}
|
||||
|
||||
return delta, nil
|
||||
}
|
||||
|
||||
func (s *trackedSubscription) recordRemovedServer(name string) {
|
||||
delete(s.materialized, name)
|
||||
}
|
||||
|
||||
func (s *trackedSubscription) recordUpdatedServer(name, tagPostfix string, serverConfig *specs.SubscriptionServerConfig) {
|
||||
s.materialized[name] = &materializedServer{tagPostfix: tagPostfix, serverConfig: serverConfig}
|
||||
}
|
||||
|
||||
func (s *trackedSubscription) getCurrentServer(name string) (*materializedServer, error) {
|
||||
if materialized, found := s.materialized[name]; found {
|
||||
return materialized, nil
|
||||
} else {
|
||||
return nil, newError("not found")
|
||||
}
|
||||
}
|
||||
|
||||
type materializedServer struct {
|
||||
tagPostfix string
|
||||
|
||||
serverConfig *specs.SubscriptionServerConfig
|
||||
}
|
@ -3,5 +3,6 @@ package environment
|
||||
type RootEnvironment interface {
|
||||
AppEnvironment(tag string) AppEnvironment
|
||||
ProxyEnvironment(tag string) ProxyEnvironment
|
||||
DropProxyEnvironment(tag string) error
|
||||
doNotImpl()
|
||||
}
|
||||
|
@ -58,6 +58,14 @@ func (r *rootEnvImpl) ProxyEnvironment(tag string) ProxyEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rootEnvImpl) DropProxyEnvironment(tag string) error {
|
||||
transientStorage, err := r.transientStorage.NarrowScope(r.ctx, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return transientStorage.DropScope(r.ctx, tag)
|
||||
}
|
||||
|
||||
type appEnvImpl struct {
|
||||
transientStorage storage.ScopedTransientStorage
|
||||
systemDialer internet.SystemDialer
|
||||
@ -83,7 +91,7 @@ func (a *appEnvImpl) Listener() internet.SystemListener {
|
||||
}
|
||||
|
||||
func (a *appEnvImpl) OutboundDialer() tagged.DialFunc {
|
||||
panic("implement me")
|
||||
return internet.DialTaggedOutbound
|
||||
}
|
||||
|
||||
func (a *appEnvImpl) OpenFileForReadSeek() fsifce.FileSeekerFunc {
|
||||
|
@ -23,6 +23,9 @@ type MessageOpt struct {
|
||||
Type []string `protobuf:"bytes,1,rep,name=type,proto3" json:"type,omitempty"`
|
||||
ShortName []string `protobuf:"bytes,2,rep,name=short_name,json=shortName,proto3" json:"short_name,omitempty"`
|
||||
TransportOriginalName string `protobuf:"bytes,86001,opt,name=transport_original_name,json=transportOriginalName,proto3" json:"transport_original_name,omitempty"`
|
||||
// allow_restricted_mode_load allow this config to be loaded in restricted mode
|
||||
// this is typically used when a an attacker can control the content
|
||||
AllowRestrictedModeLoad bool `protobuf:"varint,86002,opt,name=allow_restricted_mode_load,json=allowRestrictedModeLoad,proto3" json:"allow_restricted_mode_load,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MessageOpt) Reset() {
|
||||
@ -78,6 +81,13 @@ func (x *MessageOpt) GetTransportOriginalName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MessageOpt) GetAllowRestrictedModeLoad() bool {
|
||||
if x != nil {
|
||||
return x.AllowRestrictedModeLoad
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type FieldOpt struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@ -217,14 +227,18 @@ var file_common_protoext_extensions_proto_rawDesc = []byte{
|
||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x74, 0x1a, 0x20,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
|
||||
0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x22, 0x79, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79,
|
||||
0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x12, 0x38, 0x0a, 0x17, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x6f,
|
||||
0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0xf1, 0x9f, 0x05,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x4f,
|
||||
0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xd0, 0x02, 0x0a, 0x08,
|
||||
0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74,
|
||||
0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x4e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x38, 0x0a, 0x17, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f,
|
||||
0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0xf1, 0x9f,
|
||||
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74,
|
||||
0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x1a,
|
||||
0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x65, 0x64,
|
||||
0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0xf2, 0x9f, 0x05, 0x20, 0x01,
|
||||
0x28, 0x08, 0x52, 0x17, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63,
|
||||
0x74, 0x65, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x22, 0xd0, 0x02, 0x0a, 0x08,
|
||||
0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x6e, 0x79, 0x5f,
|
||||
0x77, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x61, 0x6e, 0x79,
|
||||
0x57, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
|
||||
|
@ -21,6 +21,10 @@ message MessageOpt{
|
||||
repeated string short_name = 2;
|
||||
|
||||
string transport_original_name = 86001;
|
||||
|
||||
// allow_restricted_mode_load allow this config to be loaded in restricted mode
|
||||
// this is typically used when a an attacker can control the content
|
||||
bool allow_restricted_mode_load = 86002;
|
||||
}
|
||||
|
||||
message FieldOpt{
|
||||
|
@ -85,8 +85,11 @@ func filterMessage(ctx context.Context, message protoreflect.Message) error {
|
||||
}
|
||||
|
||||
fsenvironment := envctx.EnvironmentFromContext(ctx)
|
||||
fsifce := fsenvironment.(filesystemcap.FileSystemCapabilitySet)
|
||||
fsifce, fsifceOk := fsenvironment.(filesystemcap.FileSystemCapabilitySet)
|
||||
for _, v := range fileReadingQueue {
|
||||
if !fsifceOk {
|
||||
return newError("unable to read file as filesystem capability is not given")
|
||||
}
|
||||
field := message.Descriptor().Fields().ByTextName(v.field)
|
||||
if v.filename == "" {
|
||||
continue
|
||||
|
@ -72,6 +72,13 @@ func (i *implementationRegistry) LoadImplementationByAlias(ctx context.Context,
|
||||
}
|
||||
|
||||
implementationConfigInstancev2 := proto.MessageV2(implementationConfigInstance)
|
||||
|
||||
if isRestrictedModeContext(ctx) {
|
||||
if err := enforceRestriction(implementationConfigInstancev2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := protofilter.FilterProtoConfig(ctx, implementationConfigInstancev2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
35
common/registry/restrict.go
Normal file
35
common/registry/restrict.go
Normal file
@ -0,0 +1,35 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/common/protoext"
|
||||
)
|
||||
|
||||
const restrictedLoadModeCtx = "restrictedLoadModeCtx"
|
||||
|
||||
func CreateRestrictedModeContext(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, restrictedLoadModeCtx, true) //nolint: staticcheck
|
||||
}
|
||||
|
||||
func isRestrictedModeContext(ctx context.Context) bool {
|
||||
v := ctx.Value(restrictedLoadModeCtx)
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
return v.(bool)
|
||||
}
|
||||
|
||||
func enforceRestriction(config proto.Message) error {
|
||||
configDescriptor := config.ProtoReflect().Descriptor()
|
||||
msgOpts, err := protoext.GetMessageOptions(configDescriptor)
|
||||
if err != nil {
|
||||
return newError("unable to find message options").Base(err)
|
||||
}
|
||||
if !msgOpts.AllowRestrictedModeLoad {
|
||||
return newError("component has not opted in for load in restricted mode")
|
||||
}
|
||||
return nil
|
||||
}
|
11
features/extension/subscription.go
Normal file
11
features/extension/subscription.go
Normal file
@ -0,0 +1,11 @@
|
||||
package extension
|
||||
|
||||
import "github.com/v2fly/v2ray-core/v5/features"
|
||||
|
||||
type SubscriptionManager interface {
|
||||
features.Feature
|
||||
}
|
||||
|
||||
func SubscriptionManagerType() interface{} {
|
||||
return (*SubscriptionManager)(nil)
|
||||
}
|
@ -9,6 +9,9 @@ var cmdEngineering = &base.Command{
|
||||
Commands: []*base.Command{
|
||||
cmdConvertPb,
|
||||
cmdReversePb,
|
||||
cmdNonNativeLinkExtract,
|
||||
cmdNonNativeLinkExec,
|
||||
cmdSubscriptionEntriesExtract,
|
||||
},
|
||||
}
|
||||
|
||||
|
54
main/commands/all/engineering/nonnativelinkexec.go
Normal file
54
main/commands/all/engineering/nonnativelinkexec.go
Normal file
@ -0,0 +1,54 @@
|
||||
package engineering
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative"
|
||||
"github.com/v2fly/v2ray-core/v5/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdNonNativeLinkExecInputName *string
|
||||
|
||||
var cmdNonNativeLinkExecTemplatePath *string
|
||||
|
||||
var cmdNonNativeLinkExec = &base.Command{
|
||||
UsageLine: "{{.Exec}} engineering nonnativelinkexec",
|
||||
Flag: func() flag.FlagSet {
|
||||
fs := flag.NewFlagSet("", flag.ExitOnError)
|
||||
cmdNonNativeLinkExecInputName = fs.String("name", "", "")
|
||||
cmdNonNativeLinkExecTemplatePath = fs.String("templatePath", "", "path for template directory (WARNING: This will not stop templates from reading file outside this directory)")
|
||||
return *fs
|
||||
}(),
|
||||
Run: func(cmd *base.Command, args []string) {
|
||||
cmd.Flag.Parse(args)
|
||||
|
||||
content, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
flattenedLink := nonnative.ExtractAllValuesFromBytes(content)
|
||||
|
||||
matcher := nonnative.NewDefMatcher()
|
||||
if *cmdNonNativeLinkExecTemplatePath != "" {
|
||||
osFs := os.DirFS(*cmdNonNativeLinkExecTemplatePath)
|
||||
err = matcher.LoadDefinitions(osFs)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
} else {
|
||||
err = matcher.LoadEmbeddedDefinitions()
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
spec, err := matcher.ExecuteNamed(flattenedLink, *cmdNonNativeLinkExecInputName)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
io.Copy(os.Stdout, bytes.NewReader(spec))
|
||||
},
|
||||
}
|
55
main/commands/all/engineering/nonnativelinkextract.go
Normal file
55
main/commands/all/engineering/nonnativelinkextract.go
Normal file
@ -0,0 +1,55 @@
|
||||
package engineering
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative"
|
||||
"github.com/v2fly/v2ray-core/v5/main/commands/base"
|
||||
)
|
||||
|
||||
type valueContainer struct {
|
||||
key, value string
|
||||
}
|
||||
|
||||
type orderedValueContainer []valueContainer
|
||||
|
||||
func (o *orderedValueContainer) Len() int {
|
||||
return len(*o)
|
||||
}
|
||||
|
||||
func (o *orderedValueContainer) Less(i, j int) bool {
|
||||
return strings.Compare((*o)[i].key, (*o)[j].key) < 0
|
||||
}
|
||||
|
||||
func (o *orderedValueContainer) Swap(i, j int) {
|
||||
(*o)[i], (*o)[j] = (*o)[j], (*o)[i]
|
||||
}
|
||||
|
||||
var cmdNonNativeLinkExtract = &base.Command{
|
||||
UsageLine: "{{.Exec}} engineering nonnativelinkextract",
|
||||
Flag: func() flag.FlagSet {
|
||||
fs := flag.NewFlagSet("", flag.ExitOnError)
|
||||
return *fs
|
||||
}(),
|
||||
Run: func(cmd *base.Command, args []string) {
|
||||
content, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
flattenedLink := nonnative.ExtractAllValuesFromBytes(content)
|
||||
var valueContainerOrdered orderedValueContainer
|
||||
|
||||
for key, value := range flattenedLink.Values {
|
||||
valueContainerOrdered = append(valueContainerOrdered, valueContainer{key, value})
|
||||
}
|
||||
sort.Sort(&valueContainerOrdered)
|
||||
for _, valueContainer := range valueContainerOrdered {
|
||||
io.WriteString(os.Stdout, fmt.Sprintf("%s=%s\n", valueContainer.key, valueContainer.value))
|
||||
}
|
||||
},
|
||||
}
|
70
main/commands/all/engineering/subscriptionEntriesExtract.go
Normal file
70
main/commands/all/engineering/subscriptionEntriesExtract.go
Normal file
@ -0,0 +1,70 @@
|
||||
package engineering
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/v2fly/v2ray-core/v5/app/subscription/containers"
|
||||
"github.com/v2fly/v2ray-core/v5/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdSubscriptionEntriesExtractInputName *string
|
||||
|
||||
var cmdSubscriptionEntriesExtract = &base.Command{
|
||||
UsageLine: "{{.Exec}} engineering subscriptionEntriesExtract",
|
||||
Flag: func() flag.FlagSet {
|
||||
fs := flag.NewFlagSet("", flag.ExitOnError)
|
||||
cmdSubscriptionEntriesExtractInputName = fs.String("input", "", "")
|
||||
return *fs
|
||||
}(),
|
||||
Run: func(cmd *base.Command, args []string) {
|
||||
cmd.Flag.Parse(args)
|
||||
inputReader := os.Stdin
|
||||
if *cmdSubscriptionEntriesExtractInputName != "" {
|
||||
file, err := os.Open(*cmdSubscriptionEntriesExtractInputName)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
inputReader = file
|
||||
defer file.Close()
|
||||
}
|
||||
content, err := io.ReadAll(inputReader)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
parsed, err := containers.TryAllParsers(content, "")
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
zipWriter := zip.NewWriter(os.Stdout)
|
||||
{
|
||||
writer, err := zipWriter.Create("meta.json")
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
err = json.NewEncoder(writer).Encode(parsed.Metadata)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
for k, entry := range parsed.ServerSpecs {
|
||||
hash := sha3.Sum256(entry.Content)
|
||||
fileName := fmt.Sprintf("entry_%v_%x", k, hash[:8])
|
||||
writer, err := zipWriter.Create(fileName)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
_, err = writer.Write(entry.Content)
|
||||
if err != nil {
|
||||
base.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
zipWriter.Close()
|
||||
},
|
||||
}
|
@ -112,4 +112,19 @@ import (
|
||||
_ "github.com/v2fly/v2ray-core/v5/proxy/shadowsocks/simplified"
|
||||
_ "github.com/v2fly/v2ray-core/v5/proxy/socks/simplified"
|
||||
_ "github.com/v2fly/v2ray-core/v5/proxy/trojan/simplified"
|
||||
|
||||
// Subscription Supports
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/subscriptionmanager"
|
||||
|
||||
// Subscription Containers: general purpose
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/containers/base64urlline"
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/containers/jsonfieldarray"
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/containers/jsonfieldarray/jsonified"
|
||||
|
||||
// Subscription Fetchers
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/documentfetcher/httpfetcher"
|
||||
|
||||
// Subscription Entries Converters
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/entries/nonnative"
|
||||
_ "github.com/v2fly/v2ray-core/v5/app/subscription/entries/outbound" // Natively Supported Outbound Format
|
||||
)
|
||||
|
12
v2ray.go
12
v2ray.go
@ -142,6 +142,18 @@ func AddOutboundHandler(server *Instance, config *OutboundHandlerConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveOutboundHandler(server *Instance, tag string) error {
|
||||
outboundManager := server.GetFeature(outbound.ManagerType()).(outbound.Manager)
|
||||
if err := outboundManager.RemoveHandler(server.ctx, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := server.env.DropProxyEnvironment("o" + tag); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addOutboundHandlers(server *Instance, configs []*OutboundHandlerConfig) error {
|
||||
for _, outboundConfig := range configs {
|
||||
if err := AddOutboundHandler(server, outboundConfig); err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user