🌴 Sync 2026-10-12 12:02:15

This commit is contained in:
github-actions[bot] committed 2026-10-12 12:02:15 +08:00
1 parent 1b64546588
commit 887e5ac8e3
261 files changed
+52646 -35012

No files matched your search

@@ -81,6 +81,35 @@ module YAML
end
end
# The pipeline loads the same large file repeatedly, cache it by mtime/size.
def self.load_file_cached(filename, cache_file, *args, **kwargs)
if File.exist?(cache_file)
begin
entry = Marshal.load(File.binread(cache_file))
if entry.is_a?(Array) && entry.length == 3 &&
entry[0] == File.mtime(filename).to_i && entry[1] == File.size(filename)
return entry[2]
end
rescue ::Exception
# ignore broken cache, parse the file again
end
end
value = load_file(filename, *args, **kwargs)
cache_write(filename, value, cache_file)
value
end
def self.cache_write(filename, value, cache_file)
begin
File.open(cache_file, 'wb') do |file|
Marshal.dump([File.mtime(filename).to_i, File.size(filename), value], file)
end
rescue ::Exception
File.delete(cache_file) rescue nil
end
end
def self.dump(obj, io = nil, **options)
if obj.nil? || obj == false
target = ""
@@ -148,22 +177,35 @@ module YAML
end
end
if io.nil?
yaml_content = original_dump(obj, **options)
needs_fix ? fix_short_id_quotes(yaml_content) : yaml_content
elsif io.respond_to?(:write)
if needs_fix
stream = options.empty? && StreamDump.supported?(obj)
if io.respond_to?(:write)
if stream
StreamDump.new(io).dump(obj, short_id: needs_fix)
elsif needs_fix
fix_short_id_text(original_dump(obj, **options), io)
else
original_dump(obj, io, **options)
end
io
elsif stream
stream_dump_to_string(obj, needs_fix)
else
yaml_content = original_dump(obj, **options)
needs_fix ? fix_short_id_quotes(yaml_content) : yaml_content
dump_string(obj, options, needs_fix)
end
end
def self.stream_dump_to_string(obj, needs_fix)
sink = StringSink.new
StreamDump.new(sink).dump(obj, short_id: needs_fix)
sink.string
end
def self.dump_string(obj, options, needs_fix)
yaml_content = original_dump(obj, **options)
needs_fix ? fix_short_id_quotes(yaml_content) : yaml_content
end
def self.dump_to_path(obj, path, **options)
real = File.symlink?(path) ? File.realpath(path) : path
dir = File.dirname(real)
@@ -173,7 +215,7 @@ module YAML
File.open(tmp, 'w') { |f| dump_to_io(obj, f, **options.merge(filename: real)) }
File.chmod(mode, tmp) if mode
File.rename(tmp, real)
rescue Exception
rescue ::Exception
begin
File.unlink(tmp) if File.exist?(tmp)
rescue
@@ -221,22 +263,49 @@ module YAML
input
end
AGE_KEYS_UCI_CONFIG = "/etc/config/openclash"
AGE_KEYS_SECTION = "config_age_secret".freeze
# The file itself says whether a lookup is needed: the common case has no age key and must
# not pay for a fork and a uci parse.
def self.age_keys_enabled?
File.read(AGE_KEYS_UCI_CONFIG).include?(AGE_KEYS_SECTION)
rescue StandardError
true
end
# One "uci show" dump resolves every name and both key types
def self.age_keys
@age_keys ||= begin
keys = {}
if age_keys_enabled?
cmd = ["/bin/sh", "-c", ". /usr/share/openclash/uci.sh; uci_age_keys_dump"]
IO.popen(cmd, "r") do |io|
io.each_line do |line|
tag, name, value = line.strip.split("\t", 3)
next if name.nil? || value.nil? || value.strip == ""
keys[name] ||= { publics: [], secrets: [] }
(tag == "P" ? keys[name][:publics] : keys[name][:secrets]) << value.strip
end
end
end
keys
rescue Errno::ENOENT
{}
end
end
def self.find_age_keys_for_filename(filename)
basename = File.basename(filename)
basename_no_ext = File.basename(filename, File.extname(filename))
keys = age_keys
publics = []
secrets = []
[basename, basename_no_ext].uniq.each do |n|
cmd_public = ["/bin/sh", "-c", ". /usr/share/openclash/uci.sh; uci_get_age_public_keys \"$1\"", "sh", n]
IO.popen(cmd_public, "r") do |io|
io.each_line { |l| publics << l.strip unless l.nil? || l.strip == "" }
end
[File.basename(filename), File.basename(filename, File.extname(filename))].uniq.each do |n|
entry = keys[n]
next if entry.nil?
cmd_secret = ["/bin/sh", "-c", ". /usr/share/openclash/uci.sh; uci_get_age_secret_keys \"$1\"", "sh", n]
IO.popen(cmd_secret, "r") do |io|
io.each_line { |l| secrets << l.strip unless l.nil? || l.strip == "" }
end
publics.concat(entry[:publics])
secrets.concat(entry[:secrets])
end
{ publics: publics, secrets: secrets }
end
@@ -451,6 +520,218 @@ module YAML
end
end
# StreamDump writes the data tree directly into libyaml, Psych.dump builds a full AST first
# and nearly doubles the peak memory. supported? skips trees with anchors or tags,
# and the scalar style rules below reproduce Psych::Visitors::YAMLTree#visit_String.
class StringSink
attr_reader :string
def initialize
@string = String.new
end
def write(data)
@string << data
data.to_s.bytesize
end
end
class StreamDump
ANY = Psych::Nodes::Scalar::ANY
PLAIN = Psych::Nodes::Scalar::PLAIN
SINGLE_QUOTED = Psych::Nodes::Scalar::SINGLE_QUOTED
DOUBLE_QUOTED = Psych::Nodes::Scalar::DOUBLE_QUOTED
LITERAL = Psych::Nodes::Scalar::LITERAL
MAP_BLOCK = Psych::Nodes::Mapping::BLOCK
SEQ_BLOCK = Psych::Nodes::Sequence::BLOCK
NULL_TAG = 'tag:yaml.org,2002:null'
STR_TAG = 'tag:yaml.org,2002:str'
def self.available?
defined?(Psych::Emitter) && defined?(Psych::ScalarScanner) && defined?(Psych::ClassLoader)
end
def self.supported?(obj)
return false unless available?
seen = {}.compare_by_identity
stack = [obj]
until stack.empty?
item = stack.pop
case item
when Hash
return false unless item.class == ::Hash
return false if seen.key?(item)
seen[item] = true
item.each do |key, value|
stack << key
stack << value
end
when Array
return false unless item.class == ::Array
return false if seen.key?(item)
seen[item] = true
stack.concat(item)
when String
return false unless item.class == ::String
return false if item.instance_variables.any?
return false if item.encoding == Encoding::ASCII_8BIT && !item.ascii_only?
when Integer, Float, TrueClass, FalseClass, NilClass
# dumped through to_s, exactly like Psych does
else
return false
end
end
true
end
def initialize(io)
@io = io
@scanner = Psych::ScalarScanner.new(Psych::ClassLoader.new)
@short_id = false
end
def dump(obj, short_id: false)
@short_id = short_id
emitter = Psych::Emitter.new(@io)
@emitter = emitter
emitter.start_stream(Psych::Parser::UTF8)
emitter.start_document([], [], false)
emit(obj)
emitter.end_document(true)
emitter.end_stream
@emitter = nil
@io
end
private
def emit(obj)
case obj
when Hash
@emitter.start_mapping(nil, nil, true, MAP_BLOCK)
obj.each do |key, value|
emit(key)
emit_value(value, key)
end
@emitter.end_mapping
when Array
@emitter.start_sequence(nil, nil, true, SEQ_BLOCK)
obj.each { |value| emit(value) }
@emitter.end_sequence
when String
emit_string(obj)
when Integer, TrueClass, FalseClass
@emitter.scalar(obj.to_s, nil, nil, true, false, ANY)
when Float
emit_float(obj)
when NilClass
@emitter.scalar('', nil, NULL_TAG, true, false, ANY)
end
end
def emit_value(value, key)
if @short_id && (key == 'short-id' || key == :'short-id')
emit_short_id(value)
else
emit(value)
end
end
def emit_float(value)
if value.nan?
@emitter.scalar('.nan', nil, nil, true, false, ANY)
elsif value.infinite?
@emitter.scalar(value.infinite? > 0 ? '.inf' : '-.inf', nil, nil, true, false, ANY)
else
@emitter.scalar(value.to_s, nil, nil, true, false, ANY)
end
end
# short-id values are always double quoted: same result as
# fix_short_id_text, without building the intermediate document string
def emit_short_id(value)
if value.is_a?(Array)
@emitter.start_sequence(nil, nil, true, SEQ_BLOCK)
value.each { |item| emit_quoted(item) }
@emitter.end_sequence
else
emit_quoted(value)
end
end
def emit_quoted(value)
case value
when String, Integer, Float
@emitter.scalar(value.to_s, nil, nil, false, true, DOUBLE_QUOTED)
when nil
@emitter.scalar('', nil, nil, false, true, DOUBLE_QUOTED)
else
emit(value)
end
end
def emit_string(value)
plain = true
quote = true
style = PLAIN
tag = nil
if value.match?(/\n(?!\Z)/)
style = LITERAL
elsif value == '<<'
style = SINGLE_QUOTED
tag = STR_TAG
plain = false
quote = false
elsif value == 'y' || value == 'Y' || value == 'n' || value == 'N'
style = DOUBLE_QUOTED
elsif value.match?(/^[^[:word:]][^"]*$/)
style = DOUBLE_QUOTED
elsif !(String === @scanner.tokenize(value)) || /\A0[0-7]*[89]/.match?(value)
style = SINGLE_QUOTED
end
@emitter.scalar(value, nil, tag, plain, quote, style)
end
end
# Inline replaces the "one Thread per field" pattern: a finished Thread keeps its stack
# until it is joined, which costs hundreds of MB for thousands of entries.
class Inline
def initialize(*args, &block)
@error = nil
begin
block.call(*args)
rescue ::Exception => e
@error = e
end
end
def join
raise @error if @error
self
end
def value
raise @error if @error
self
end
def alive?
false
end
def status
@error ? nil : false
end
def kill
self
end
alias_method :terminate, :kill
end
def self.overwrite(base, override)
return override if base.nil?
return base if override.nil?
@@ -770,4 +1051,405 @@ module YAML
raise "[batch update] update_spec: [#{update_spec}], error: [#{e.message}]"
end
end
# The [Overwrite] modules and the custom overwrite script are both executed by this section,
# they record their helper calls and let one process own one Value and one dump; the argument
# counts below must stay in sync with ruby_record() in ruby.sh
OVERWRITE_HELPERS = {
'ruby_arr_add_file' => 5,
'ruby_arr_edit' => 6,
'ruby_arr_head_add_file' => 4,
'ruby_arr_insert' => 4,
'ruby_arr_insert_arr' => 4,
'ruby_arr_insert_hash' => 4,
'ruby_cover' => 4,
'ruby_delete' => 3,
'ruby_edit' => 3,
'ruby_map_edit' => 5,
'ruby_merge' => 4,
'ruby_merge_hash' => 3,
'ruby_uniq' => 2
}.freeze
OVERWRITE_REQUIRED = {
'ruby_arr_add_file' => [1, 2, 3, 4, 5],
'ruby_arr_edit' => [1, 2],
'ruby_arr_head_add_file' => [1, 2, 3, 4],
'ruby_arr_insert' => [1, 2, 3, 4],
'ruby_arr_insert_arr' => [1, 2, 3, 4],
'ruby_arr_insert_hash' => [1, 2, 3, 4],
'ruby_cover' => [1, 2],
'ruby_delete' => [1, 3],
'ruby_edit' => [1, 2],
'ruby_map_edit' => [1, 2, 3, 4],
'ruby_merge' => [1, 2, 3],
'ruby_merge_hash' => [1, 2, 3],
'ruby_uniq' => [1, 2]
}.freeze
# The code arguments are checked against these lists before eval: node types give the allowed
# syntax, the method, operator and constant lists the allowed callable surface, everything
# not listed is rejected; the inspected arguments never take part in the check
OVERWRITE_NODES = %i[
AND ARGS ATTRASGN BEGIN BLOCK BLOCK_PASS BREAK CALL CASE CASE3 CONST DASGN DOT2 DOT3
DREGX DSTR DVAR EVSTR FALSE FCALL HASH IF IN ITER LAMBDA LASGN LIST LIT LVAR MASGN
NEXT NIL NOT OP_ASGN1 OP_ASGN2 OP_ASGN_AND OP_ASGN_OR OPCALL OR RESBODY RESCUE SCOPE
STR SYM TRUE UNLESS WHEN ZLIST
].freeze
OVERWRITE_METHODS = %w(
[] []= LOG LOG_ERROR LOG_TIP LOG_WARN abs all? any? call capitalize ceil chars chomp
chomp! chop clone compact compact! concat count delete delete_at delete_if detect dig
downcase drop drop_while dup each each_char each_key each_line each_pair each_slice
each_value each_with_index empty? end_with? fetch filter filter! find find_index first
flat_map flatten flatten! floor frozen? group_by gsub gsub! has_key? has_value?
include? index insert inspect instance_of? is_a? itself join keep_if key? keys kind_of?
lambda last length lstrip map map! match match? max max_by member? merge merge! min
min_by nil? none? partition pop proc push reject reject! replace reverse reverse!
rindex round rstrip select select! shift size slice slice! sort sort! sort_by sort_by!
split start_with? store strip sub sub! sum swapcase take take_while tally tap then to_a
to_f to_h to_i to_s to_sym transform_keys transform_keys! transform_values
transform_values! uniq uniq! unshift upcase update values values_at yield_self
).freeze
OVERWRITE_OPERATORS = %w[! != !~ % & * ** + - / < << <= <=> == =~ > >= >> ^ | ~].freeze
OVERWRITE_CONSTANTS = %w[Array FalseClass Float Hash Integer NilClass Numeric String Symbol TrueClass Value YAML].freeze
# Derived lookups keep the node walk cheap, the tables above stay readable
OVERWRITE_NODE_LOOKUP = OVERWRITE_NODES.to_h { |type| [type, true] }.freeze
OVERWRITE_CALL_LOOKUP = (OVERWRITE_METHODS + OVERWRITE_OPERATORS).to_h { |name| [name.to_sym, true] }.freeze
OVERWRITE_CONST_LOOKUP = OVERWRITE_CONSTANTS.to_h { |name| [name.to_sym, true] }.freeze
# Argument numbers follow the helper call order of ruby.sh, 1 is the config file; the check
# only covers the arguments interpolated as ruby source
OVERWRITE_CODE = {
'ruby_arr_add_file' => [2, 3, 5],
'ruby_arr_edit' => [2, 3, 5],
'ruby_arr_head_add_file' => [2, 4],
'ruby_arr_insert' => [2, 3],
'ruby_arr_insert_arr' => [2, 3, 4],
'ruby_arr_insert_hash' => [2, 3, 4],
'ruby_cover' => [2],
'ruby_delete' => [2],
'ruby_edit' => [2, 3],
'ruby_map_edit' => [2, 4],
'ruby_merge' => [2, 4],
'ruby_merge_hash' => [2, 3],
'ruby_uniq' => [2]
}.freeze
# Manifest records: 'M <module>' opens a module block, 'Y <file>' is its [YAML] override block
# and every 'L <line>' is one [Overwrite] line of it
def self.overwrite_run(config_file, manifest_file)
return if config_file.nil? || config_file.empty? || !File.exist?(manifest_file)
# module lines expand $CONFIG_FILE from the environment, keep the legacy contract
ENV['CONFIG_FILE'] = config_file
value = load_file(config_file)
blocks = []
File.foreach(manifest_file) do |raw|
line = raw.chomp
if line.start_with?('M ')
blocks << [line[2..-1], nil, []]
elsif blocks.any?
if line.start_with?('Y ')
blocks.last[1] = line[2..-1]
elsif line.start_with?('L ')
blocks.last[2] << line[2..-1]
end
end
end
blocks.each do |name, yaml_file, lines|
value = overwrite_apply_yaml(value, yaml_file) if yaml_file
lines.each { |line| overwrite_apply_line(value, name, line) }
end
dump(value, config_file)
rescue ::Exception => e
LOG_ERROR("Set Custom Overwrite Script Failed,【#{e.message}】")
end
# Calls recorded by ruby_record() in ruby.sh: NUL separated fields and a fixed argument count
# per helper, the records may target several config files
def self.overwrite_run_custom(calls_file)
return if calls_file.nil? || calls_file.empty? || !File.exist?(calls_file)
fields = File.read(calls_file).split("\0", -1)
values = {}
position = 0
while position < fields.length
fn = fields[position]
arity = OVERWRITE_HELPERS[fn]
if fn.nil? || fn.empty?
position += 1
next
elsif arity.nil?
LOG_WARN("skip unsafe Overwrite command【Ruby Script => #{fn}】")
position += 1
next
end
args = fields[position + 1, arity] || []
args += [''] * (arity - args.length)
position += arity + 1
if overwrite_unsafe?(fn, args)
LOG_WARN("skip unsafe Overwrite command【Ruby Script => #{fn}(#{args.map(&:inspect).join(', ')})】")
next
end
file = args[0]
next if file.nil? || file.empty? || !File.exist?(file)
values[file] = load_file(file) unless values.key?(file)
begin
overwrite_apply(values[file], fn, args)
rescue ::Exception => e
LOG_ERROR("Set Custom Overwrite Script Failed,【#{e.message}】")
end
end
values.each { |file, value| dump(value, file) }
rescue ::Exception => e
LOG_ERROR("Set Custom Overwrite Script Failed,【#{e.message}】")
end
def self.overwrite_apply_yaml(value, yaml_file)
return value if yaml_file.nil? || yaml_file.empty? || !File.exist?(yaml_file)
begin
yaml_data = load(File.read(yaml_file))
if yaml_data.is_a?(Hash)
value = overwrite(value, yaml_data)
else
LOG_WARN('Invalid YAML Override format, skipped...')
end
rescue ::Exception => e
LOG_ERROR("Parse YAML Override failed:【#{e.message}】")
end
value
end
def self.overwrite_apply_line(value, name, line)
parsed = overwrite_parse_line(line)
if parsed.nil?
LOG_WARN("skip invalid Overwrite command【Ruby Script => #{name}: #{line}】")
return
end
fn, args = parsed
if overwrite_unsafe?(fn, args)
LOG_WARN("skip invalid Overwrite command【Ruby Script => #{name}: #{line}】")
return
end
LOG_TIP("Load Overwrite Script【Ruby Script => #{line}】")
overwrite_apply(value, fn, args)
rescue ::Exception => e
LOG_ERROR("Set Custom Overwrite Script Failed,【#{e.message}】")
end
def self.overwrite_unsafe?(fn, args)
OVERWRITE_CODE[fn].any? do |index|
text = args[index - 1]
!text.nil? && !text.empty? && overwrite_code_unsafe?(text)
end
end
# The hash body of ruby_merge_hash is only valid inside braces, so a => fragment needs the
# braced source checked too, an unparsable argument is unsafe
def self.overwrite_code_unsafe?(text)
tree = overwrite_parse_source(text)
if tree.nil?
tree = overwrite_parse_source("{#{text}}")
return true if tree.nil?
elsif text.include?('=>')
braced = overwrite_parse_source("{#{text}}")
return true if braced && overwrite_ast_unsafe?(braced)
end
overwrite_ast_unsafe?(tree)
end
def self.overwrite_parse_source(source)
RubyVM::AbstractSyntaxTree.parse(source)
rescue ::Exception
nil
end
# An explicit stack keeps the walk free of per-node recursion and block calls
def self.overwrite_ast_unsafe?(node)
stack = [node]
until stack.empty?
current = stack.pop
next unless current.is_a?(RubyVM::AbstractSyntaxTree::Node)
return true unless OVERWRITE_NODE_LOOKUP.key?(current.type)
case current.type
when :CALL, :FCALL, :VCALL, :ATTRASGN
return true unless overwrite_method_allowed?(overwrite_call_name(current))
when :OPCALL
return true unless overwrite_method_allowed?(current.children[1])
when :CONST
names = overwrite_leaf_names(current)
return true unless names.length == 1 && OVERWRITE_CONST_LOOKUP.key?(names[0].to_sym)
when :BLOCK_PASS
names = overwrite_leaf_names(current)
return true if names.empty? || names.any? { |name| !overwrite_method_allowed?(name) }
end
stack.concat(current.children)
end
false
end
def self.overwrite_method_allowed?(name)
return OVERWRITE_CALL_LOOKUP.key?(name) if name.is_a?(Symbol)
OVERWRITE_CALL_LOOKUP.key?(name.to_s.to_sym)
end
def self.overwrite_call_name(node)
(node.type == :CALL || node.type == :ATTRASGN) ? node.children[1] : node.children[0]
end
# Symbols and strings of a node subtree, the called name of &:<name> block passes and the
# name of constants are carried this way
def self.overwrite_leaf_names(node, names = [])
node.children.each do |child|
if child.is_a?(Symbol) || child.is_a?(String)
names << child
elsif child.is_a?(RubyVM::AbstractSyntaxTree::Node)
overwrite_leaf_names(child, names)
end
end
names
end
# Only a single call to an allowed helper with quoted arguments is accepted; in double quoted
# arguments $NAME and ${NAME} expand from the environment and every other character stays
# literal, in single quoted arguments nothing is expanded
def self.overwrite_parse_line(line)
length = line.length
position = 0
position += 1 while position < length && (line[position] == ' ' || line[position] == "\t")
head = position
position += 1 while position < length && line[position] != ' ' && line[position] != "\t"
fn = line[head...position]
return nil unless OVERWRITE_HELPERS.key?(fn)
args = []
while position < length
position += 1 while position < length && (line[position] == ' ' || line[position] == "\t")
break if position >= length
quote = line[position]
return nil unless quote == '"' || quote == "'"
position += 1
closing = line.index(quote, position)
return nil if closing.nil?
arg = line[position...closing]
args << (quote == '"' && arg.include?('$') ? overwrite_expand_env(arg) : arg)
position = closing + 1
end
return nil if args.empty?
[fn, args]
end
def self.overwrite_expand_env(text)
text.gsub(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/) do
ENV[Regexp.last_match(1) || Regexp.last_match(2)].to_s
end
end
def self.overwrite_apply(value, fn, args)
arity = OVERWRITE_HELPERS[fn]
args = args[0, arity] || []
args += [''] * (arity - args.length)
return if OVERWRITE_REQUIRED[fn].any? { |index| args[index - 1].empty? }
statement = overwrite_statement(fn, args)
return if statement.nil?
overwrite_eval(value, statement)
end
def self.overwrite_statement(fn, args)
path = args[1]
a3 = args[2]
a4 = args[3]
a5 = args[4]
a6 = args[5]
case fn
when 'ruby_edit'
"Value#{path} = #{a3}"
when 'ruby_cover'
"if File.exist?(#{a3.inspect}) then value_1 = YAML.load_file(#{a3.inspect}); if not #{a4.inspect}.empty? then Value#{path} = value_1[#{a4.inspect}] else Value#{path} = value_1 end else if not #{a4.inspect}.empty? then Value.delete(#{a4.inspect}); end; end"
when 'ruby_merge'
"if File.exist?(#{a3.inspect}) then value_1 = YAML.load_file(#{a3.inspect}); if not Value#{path} then Value#{path} = {}; end; if value_1#{a4} && value_1#{a4}.is_a?(Hash) then Value#{path}.merge!(value_1#{a4}) end end"
when 'ruby_uniq'
"if Value#{path} then Value#{path} = Value#{path}.uniq; end"
when 'ruby_merge_hash'
"if not Value#{path} then Value#{path} = {}; end; Value#{path}.merge!({#{a3}})"
when 'ruby_arr_add_file'
"if File.exist?(#{a4.inspect}) then value_1 = YAML.load_file(#{a4.inspect}); if not Value#{path} or Value#{path}.nil? then Value#{path} = []; end; if value_1#{a5} && value_1#{a5}.is_a?(Array) then idx = [#{a3}.to_i, 0].max; idx = [idx, Value#{path}.length].min; value_1#{a5}.reverse.each{|x| Value#{path}.insert(idx,x); idx += 1}; Value#{path} = Value#{path}.uniq end end"
when 'ruby_arr_head_add_file'
"if File.exist?(#{a3.inspect}) then value_1 = YAML.load_file(#{a3.inspect}); if not Value#{path} or Value#{path}.nil? then Value#{path} = []; end; if value_1#{a4} && value_1#{a4}.is_a?(Array) then Value#{path} = (value_1#{a4} + Value#{path}).uniq else Value#{path} = Value#{path}.uniq end end"
when 'ruby_arr_insert'
"if not Value#{path} or Value#{path}.nil? then Value#{path} = []; end; idx = [#{a3}.to_i, 0].max; idx = [idx, Value#{path}.length].min; Value#{path} = Value#{path}.insert(idx, #{a4.inspect}).uniq"
when 'ruby_arr_insert_hash'
"if not Value#{path} or Value#{path}.nil? then Value#{path} = []; end; idx = [#{a3}.to_i, 0].max; idx = [idx, Value#{path}.length].min; Value#{path} = Value#{path}.insert(idx, #{a4}).uniq"
when 'ruby_arr_insert_arr'
"if not Value#{path} or Value#{path}.nil? then Value#{path} = []; end; if (#{a4}).is_a?(Array) then idx = [#{a3}.to_i, 0].max; idx = [idx, Value#{path}.length].min; (#{a4}).reverse.each{|x| Value#{path} = Value#{path}.insert(idx,x); idx += 1}; Value#{path} = Value#{path}.uniq end"
when 'ruby_delete'
if path.empty?
"Value.delete(#{a3.inspect})"
else
"if Value#{path} then if Value#{path}.is_a?(Hash) then Value#{path}.delete(#{a3.inspect}) elsif Value#{path}.is_a?(Array) then Value#{path}.delete(#{a3.inspect}) end end"
end
when 'ruby_map_edit'
"if Value#{path} && Value#{path}.is_a?(Hash) then if Value#{path}[#{a3.inspect}] && Value#{path}[#{a3.inspect}].is_a?(Hash) then Value#{path}[#{a3.inspect}]#{a4} = #{a5.inspect} end end"
when 'ruby_arr_edit'
if !a3.empty? && !a4.empty? && !a5.empty? && !a6.empty?
"if Value#{path} && Value#{path}.is_a?(Array) then Value#{path}.map!{|x| if x.is_a?(Hash) && x#{a3} == #{a4.inspect} then x#{a5} = #{a6.inspect} end; x}; Value#{path}.uniq! end"
elsif a3.empty? && !a4.empty? && a5.empty? && !a6.empty?
"if Value#{path} && Value#{path}.is_a?(Array) then Value#{path}.map!{|x| if x == #{a4.inspect} then #{a6.inspect} else x end}; Value#{path}.uniq! end"
end
end
end
def self.overwrite_eval(value, statement)
remove_const(:Value) if const_defined?(:Value, false)
const_set(:Value, value)
eval(statement, binding, '(overwrite)', 1)
end
# Value readers for scripts, the path keeps the ruby source form the old helpers accepted,
# e.g. "['dns']['fake-ip-range']" or ".select { |k, _| k != 'proxies' }.to_yaml"
def self.overwrite_read(file, path)
value = load_file(file)
result = overwrite_eval(value, "Value#{path}")
puts result if result
rescue
nil
end
def self.overwrite_read_hash(code, path)
overwrite_eval(nil, "v = #{code}; if v#{path} then puts v#{path} end")
rescue
nil
end
def self.overwrite_read_hash_arr(file, path, item_path)
overwrite_eval(nil, "v = YAML.load_file(#{file.inspect}); if v#{path} then v#{path}.each do |i| if i#{item_path} then puts i#{item_path} end; end; end")
rescue
nil
end
end
@@ -2,49 +2,49 @@
START_LOG="/tmp/openclash_start.log"
LOG_FILE="/tmp/openclash.log"
LOG_OUT()
{
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Info] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Info] ${1}" >> $LOG_FILE
fi
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Info] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Info] ${1}" >> $LOG_FILE
fi
}
LOG_TIP()
{
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Tip] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Tip] ${1}" >> $LOG_FILE
fi
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Tip] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Tip] ${1}" >> $LOG_FILE
fi
}
LOG_WARN()
{
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Warning] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Warning] ${1}" >> $LOG_FILE
fi
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Warning] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Warning] ${1}" >> $LOG_FILE
fi
}
LOG_ERROR()
{
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Error] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Error] ${1}" >> $LOG_FILE
fi
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Error] ${1}" >> $START_LOG
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Error] ${1}" >> $LOG_FILE
fi
}
LOG_INFO()
{
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Info] ${1}" >> $LOG_FILE
fi
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Info] ${1}" >> $LOG_FILE
fi
}
LOG_WATCHDOG()
{
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Watchdog] ${1}" >> $LOG_FILE
fi
if [ -n "${1}" ]; then
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [Watchdog] ${1}" >> $LOG_FILE
fi
}
@@ -29,6 +29,9 @@ CLASH="/etc/openclash/clash"
CLASH_CONFIG="/etc/openclash"
restart=0
only_download=0
norestart=0
exit_code=0
[ "$2" = "norestart" ] && norestart=1
inc_job_counter
@@ -106,18 +109,18 @@ DOWNLOAD_RESULT=$?
config_cus_up()
{
if [ -z "$subscribe_url_param" ]; then
if [ -n "$key_match_param" ] || [ -n "$key_ex_match_param" ]; then
LOG_OUT "Config File【$name】Start Picking Nodes..."
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
begin
if [ -z "$subscribe_url_param" ]; then
if [ -n "$key_match_param" ] || [ -n "$key_ex_match_param" ]; then
LOG_OUT "Config File【$name】Start Picking Nodes..."
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
begin
threads = [];
Value = YAML.load_file('$CFG_FILE');
if Value.has_key?('proxies') and not Value['proxies'].to_a.empty? then
Value['proxies'].reverse.each{
|x|
Value = YAML.load_file('$CFG_FILE');
if Value.has_key?('proxies') and not Value['proxies'].to_a.empty? then
Value['proxies'].reverse.each{
|x|
if not '$key_match_param'.empty? then
threads << Thread.new {
threads << YAML::Inline.new {
if not /$key_match_param/i =~ x['name'] then
Value['proxies'].delete(x)
Value['proxy-groups'].each{
@@ -133,7 +136,7 @@ config_cus_up()
};
end;
if not '$key_ex_match_param'.empty? then
threads << Thread.new {
threads << YAML::Inline.new {
if /$key_ex_match_param/i =~ x['name'] then
if Value['proxies'].include?(x) then
Value['proxies'].delete(x)
@@ -150,12 +153,12 @@ config_cus_up()
end;
};
end;
};
end;
};
end;
if Value.key?('proxy-providers') and not Value['proxy-providers'].nil? then
Value['proxy-providers'].values.each do
|i|
threads << Thread.new {
threads << YAML::Inline.new {
if not '$key_match_param'.empty? then
i['filter'] = '(?i)$key_match_param';
end;
@@ -166,16 +169,16 @@ config_cus_up()
end;
end;
threads.each(&:join);
rescue Exception => e
YAML.LOG_ERROR('Filter Proxies Failed,【' + e.message + '】');
ensure
begin
YAML.dump(Value, '$CFG_FILE');
rescue Exception => e
YAML.LOG_ERROR('Write file failed:【%s】' % [e.message])
end
end" 2>/dev/null >> $LOG_FILE
fi
rescue Exception => e
YAML.LOG_ERROR('Filter Proxies Failed,【' + e.message + '】');
ensure
begin
YAML.dump(Value, '$CFG_FILE');
rescue Exception => e
YAML.LOG_ERROR('Write file failed:【%s】' % [e.message])
end
end" 2>/dev/null >> $LOG_FILE
fi
fi
}
@@ -208,8 +211,8 @@ config_su_check()
if [ -z "$CONFIG_PATH" ]; then
uci -q set openclash.config.config_path="$CONFIG_FILE"
uci commit openclash
fi
if [ "$CONFIG_FILE" == "$CONFIG_PATH" ]; then
fi
if [ "$CONFIG_FILE" == "$CONFIG_PATH" ] && [ "$norestart" -eq 0 ]; then
restart=1
fi
}
@@ -218,13 +221,14 @@ config_error()
{
LOG_ERROR "【$name】Update Error, Please Try Again Later..."
rm -rf "$CFG_FILE" 2>/dev/null
exit_code=1
}
change_dns()
{
if pidof clash >/dev/null; then
/etc/init.d/openclash reload "restore" >/dev/null 2>&1
procd_send_signal "openclash" "openclash-watchdog" CONT
procd_send_signal "openclash-watchdog" "" CONT
fi
}
@@ -232,7 +236,7 @@ config_download_direct()
{
if pidof clash >/dev/null && [ "$router_self_proxy" = 1 ]; then
kill_streaming_unlock
procd_send_signal "openclash" "openclash-watchdog" STOP
procd_send_signal "openclash-watchdog" "" STOP
/etc/init.d/openclash reload "revert" >/dev/null 2>&1
sleep 3
@@ -279,12 +283,12 @@ config_download_direct()
server_key_match()
{
local key_match key_word
local key_match key_word
if [ -n "$(echo "$1" |grep "^ \{0,\}$")" ] || [ -n "$(echo "$1" |grep "^\t\{0,\}$")" ]; then
return
return
fi
if [ -n "$(echo "$1" |grep "&")" ]; then
key_word=$(echo "$1" |sed 's/&/ /g')
for k in $key_word
@@ -309,7 +313,7 @@ server_key_match()
key_match_param="$key_match_param|$key_match"
fi
elif [ "$2" = "ex_keyword" ]; then
if [ -z "$key_ex_match_param" ]; then
if [ -z "$key_ex_match_param" ]; then
key_ex_match_param="$key_match"
else
key_ex_match_param="$key_ex_match_param|$key_match"
@@ -349,6 +353,7 @@ sub_info_get()
config_get "address" "$section" "address" ""
config_get "keyword" "$section" "keyword" ""
config_get "ex_keyword" "$section" "ex_keyword" ""
config_get_bool "keyword_option" "$section" "keyword_option" "1"
config_get "emoji" "$section" "emoji" ""
config_get "udp" "$section" "udp" ""
config_get "skip_cert_verify" "$section" "skip_cert_verify" ""
@@ -357,6 +362,8 @@ sub_info_get()
config_get "template" "$section" "template" ""
config_get "node_type" "$section" "node_type" ""
config_get "rule_provider" "$section" "rule_provider" ""
config_get "tfo" "$section" "tfo" ""
config_get "tls13" "$section" "tls13" ""
config_get "custom_template_url" "$section" "custom_template_url" ""
config_get "de_ex_keyword" "$section" "de_ex_keyword" ""
config_get "sub_ua" "$section" "sub_ua" "clash-verge/v2.4.5"
@@ -392,24 +399,38 @@ sub_info_get()
rule_provider=""
fi
if [ "$tfo" == "true" ]; then
tfo="&tfo=true"
else
tfo=""
fi
if [ "$tls13" == "true" ]; then
tls13="&tls13=true"
else
tls13=""
fi
if [ -n "$2" ] && [ "$2" != "$CONFIG_FILE" ] && [ "$2" != "$name" ]; then
return
fi
if [ ! -z "$keyword" ] || [ ! -z "$ex_keyword" ]; then
config_list_foreach "$section" "keyword" server_key_match "keyword"
config_list_foreach "$section" "ex_keyword" server_key_match "ex_keyword"
fi
if [ "$keyword_option" -eq 1 ]; then
if [ ! -z "$keyword" ] || [ ! -z "$ex_keyword" ]; then
config_list_foreach "$section" "keyword" server_key_match "keyword"
config_list_foreach "$section" "ex_keyword" server_key_match "ex_keyword"
fi
if [ -n "$de_ex_keyword" ]; then
for i in $de_ex_keyword;
do
if [ -z "$key_ex_match_param" ]; then
key_ex_match_param="($i)"
else
key_ex_match_param="$key_ex_match_param|($i)"
fi
done
if [ -n "$de_ex_keyword" ]; then
for i in $de_ex_keyword;
do
if [ -z "$key_ex_match_param" ]; then
key_ex_match_param="($i)"
else
key_ex_match_param="$key_ex_match_param|($i)"
fi
done
fi
fi
if [ "$sub_convert" -eq 0 ]; then
@@ -429,7 +450,7 @@ sub_info_get()
template_path_encode=$(urlencode "$template_path")
[ -n "$key_match_param" ] && key_match_param="$(urlencode "(?i)$key_match_param")"
[ -n "$key_ex_match_param" ] && key_ex_match_param="$(urlencode "(?i)$key_ex_match_param")"
subscribe_url_param="?target=clash&new_name=true&url=$subscribe_url&config=$template_path_encode&include=$key_match_param&exclude=$key_ex_match_param&emoji=$emoji&list=false&sort=$sort$udp&scv=$skip_cert_verify&append_type=$node_type&fdn=true$rule_provider$append_custom_params"
subscribe_url_param="?target=clash&new_name=true&url=$subscribe_url&config=$template_path_encode&include=$key_match_param&exclude=$key_ex_match_param&emoji=$emoji&list=false&sort=$sort$udp&scv=$skip_cert_verify&append_type=$node_type&fdn=true$rule_provider$tfo$tls13$append_custom_params"
c_address="$convert_address"
else
subscribe_url=$address
@@ -476,4 +497,7 @@ sub_info_get()
config_load "openclash"
config_foreach sub_info_get "config_subscribe" "$1"
dec_job_counter_and_restart "$restart"
del_lock
del_lock
# the caller (luci) judges the update result by this exit code
exit "$exit_code"
@@ -2,6 +2,8 @@
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_etag.sh
DEFAULT_UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36"
DOWNLOAD_FAILURE_OUTPUT() {
failure_exit_code="$1"
failure_http_code="$2"
@@ -26,7 +28,7 @@ DOWNLOAD_FILE_CURL() {
DOWNLOAD_UA=$4
SECRET_KEY=$5
CUSTOM_HEADERS=$6
[ -z "$DOWNLOAD_UA" ] && DOWNLOAD_UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
[ -z "$DOWNLOAD_UA" ] && DOWNLOAD_UA="$DEFAULT_UA"
HEADER_TMP="/tmp/openclash_curl_header_$$"
DOWNLOAD_TMP="${DOWNLOAD_PATH}.download.$$"
CACHED_ETAG=$(GET_ETAG_BY_PATH "$FILE_PATH")
@@ -191,6 +193,14 @@ EOF
fi
fi
resolve_checksum_from_url "$DOWNLOAD_URL"
if ! verify_sha256_checksum "$DOWNLOAD_TMP" "$CHECKSUM_FILENAME" "$CHECKSUM_URL" "$DOWNLOAD_UA"; then
LOG_OUT "【${DOWNLOAD_PATH}】Checksum Verification Failed"
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
return 1
fi
if ! mv -f "$DOWNLOAD_TMP" "$DOWNLOAD_PATH"; then
LOG_OUT "【${DOWNLOAD_PATH}】Download Failed:【Unable to save download file】"
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
@@ -206,3 +216,83 @@ EOF
return 0
}
CHECKSUM_RAW_PREFIX="https://raw.githubusercontent.com/vernesong/OpenClash"
CHECKSUM_OIX_URL="https://github.com/vernesong/mihomo-oix/releases/download/Pre-Alpha/checksums.txt"
resolve_checksum_from_url() {
local url="$1"
local base name path
CHECKSUM_URL=""
CHECKSUM_FILENAME=""
[ -z "$url" ] && return 1
base="${url%%\?*}"
base="${base%%#*}"
name="${base##*/}"
[ -n "$name" ] || return 1
case "$name" in
clash-*.tar.gz|luci-app-openclash_*.ipk|luci-app-openclash-*.apk|mihomo-*.gz) ;;
*)
return 1
;;
esac
case "$base" in
*"jsdelivr.net/gh/vernesong/OpenClash@"*)
path="${base#*OpenClash@}"
[ "${path%/*}" = "$path" ] && return 1
CHECKSUM_URL="${CHECKSUM_RAW_PREFIX}/${path%/*}/checksums.txt"
;;
*"raw.githubusercontent.com/vernesong/OpenClash/"*)
path="${base#*OpenClash/}"
[ "${path%/*}" = "$path" ] && return 1
CHECKSUM_URL="${CHECKSUM_RAW_PREFIX}/${path%/*}/checksums.txt"
;;
*"mihomo-oix/"*)
CHECKSUM_URL="$CHECKSUM_OIX_URL"
;;
*)
return 1
;;
esac
CHECKSUM_FILENAME="$name"
return 0
}
verify_sha256_checksum() {
local file="$1"
local expected_name="$2"
local checksum_url="$3"
local ua="${4:-$DEFAULT_UA}"
[ -z "$file" ] || [ -z "$checksum_url" ] && return 0
[ -s "$file" ] || return 0
local expected_hash
expected_hash=$(curl -sL -m 15 --connect-timeout 5 -H "User-Agent: $ua" "$checksum_url" 2>/dev/null \
| awk -v n="$expected_name" '$2 ~ n {print $1; exit} $0 ~ n {print $1; exit}')
if [ -z "$expected_hash" ]; then
LOG_WARN "Checksum file unavailable or entry not found, skip verification for【$expected_name】"
return 0
fi
local actual_hash
actual_hash=$(sha256sum "$file" 2>/dev/null | awk '{print $1}')
if [ -z "$actual_hash" ]; then
LOG_WARN "Unable to compute checksum, skip verification for【$expected_name】"
return 0
fi
if [ "$actual_hash" = "$expected_hash" ]; then
LOG_OUT "Checksum Verification Successful for【$expected_name】"
return 0
fi
LOG_ERROR "Checksum mismatch for【$expected_name】(expected【$expected_hash】,got【$actual_hash】)"
return 1
}
@@ -30,8 +30,8 @@ if [ "$(uci_get_config "enable_custom_domain_dns_server")" = "1" ] && [ "$(uci_g
custom_domain_dns_server=$(uci_get_config "custom_domain_dns_server")
[ -z "$custom_domain_dns_server" ] && {
custom_domain_dns_server="114.114.114.114"
}
custom_domain_dns_server="114.114.114.114"
}
if [ -s "/etc/openclash/custom/openclash_custom_domain_dns.list" ]; then
mkdir -p ${DNSMASQ_CONF_DIR}
@@ -28,7 +28,7 @@ DEBUG_LOG="/tmp/openclash_debug.log"
LOGTIME=$(echo $(date "+%Y-%m-%d %H:%M:%S"))
log_level=$(uci_get_config "log_level")
enable_custom_dns=$(uci_get_config "enable_custom_dns")
enable_custom_clash_rules=$(uci_get_config "enable_custom_clash_rules")
enable_custom_clash_rules=$(uci_get_config "enable_custom_clash_rules")
ipv6_enable=$(uci_get_config "ipv6_enable")
ipv6_dns=$(uci_get_config "ipv6_dns")
enable_redirect_dns=$(uci_get_config "enable_redirect_dns")
@@ -49,6 +49,11 @@ fi
core_meta_version=$(/etc/openclash/core/clash_meta -v 2>/dev/null |awk -F ' ' '{print $3}' |head -1 2>/dev/null)
op_version=$(ipk_v "luci-app-openclash")
china_ip_route=$(uci_get_config "china_ip_route")
china_ip_route_domain_source=$(uci_get_config "china_ip_route_domain_source" || echo "mrs")
case "$china_ip_route_domain_source" in
mrs|geosite) ;;
*) china_ip_route_domain_source="mrs" ;;
esac
common_ports=$(uci_get_config "common_ports")
router_self_proxy=$(uci_get_config "router_self_proxy")
core_type=$(uci_get_config "core_type" || echo "Dev")
@@ -89,28 +94,28 @@ fi
ts_cf()
{
if [ "$1" = "0" ] || [ -z "$1" ]; then
echo "停用"
else
echo "启用"
if [ "$1" = "0" ] || [ -z "$1" ]; then
echo "停用"
else
echo "启用"
fi
}
ts_re()
{
if [ -z "$1" ]; then
echo "未安装"
else
echo "已安装 ($1)"
if [ -z "$1" ]; then
echo "未安装"
else
echo "已安装 ($1)"
fi
}
dns_re()
{
if [ "$1" = "1" ]; then
echo "Dnsmasq 转发"
echo "Dnsmasq 转发"
elif [ "$1" = "2" ]; then
echo "Firewall 转发"
echo "Firewall 转发"
else
echo "停用"
fi
@@ -146,6 +151,7 @@ cat > "$DEBUG_LOG" <<-EOF
| 系统运行时间 | $(uptime 2>/dev/null) |
| IPV6-DHCP | $(uci -q get dhcp.lan.dhcpv6 || echo "未配置") |
| DNS劫持 | $(dns_re "$enable_redirect_dns") |
| TFO 黑洞超时(秒) | $(cat /proc/sys/net/ipv4/tcp_fastopen_blackhole_timeout_sec 2>/dev/null || echo "不支持") |
### 磁盘与内存
@@ -311,8 +317,9 @@ cat >> "$DEBUG_LOG" <<-EOF
| 仅代理命中规则流量 | $(ts_cf "$enable_rule_proxy") |
| 仅允许常用端口流量 | $(ts_cf "$common_ports") |
| 绕过中国大陆IP | $(ts_cf "$china_ip_route") |
| 中国大陆域名数据源 | $china_ip_route_domain_source |
| 路由本机代理 | $(ts_cf "$router_self_proxy") |
| TUN堆栈类型 | ${stack_type:-system} |
| TUN堆栈类型 | ${stack_type:-mips} |
| 启动延迟 | ${delay_start:-0}秒 |
| 日志大小 | ${log_size:-1024}KB |
| 旁路由兼容 | $(ts_cf "$bypass_gateway_compatible") |
@@ -800,4 +807,4 @@ sed -i -E 's/([0-9a-fA-F]{1,4})(:[0-9a-fA-F]{1,4}){0,5}::([0-9a-fA-F]{1,4}(:[0-9
sed -i 's/Downloading URL【[^】]*】/Downloading URL【*】/g' "$DEBUG_LOG" 2>/dev/null
del_lock
del_lock
@@ -13,7 +13,7 @@ local resolve = arg[2]
local function debug_dns()
local info, ip, host
ip = fs.lanip()
ip = fs.lanip(true)
local port = fs.uci_get_config("config", "cn_port")
local passwd = fs.uci_get_config("config", "dashboard_password") or ""
@@ -9,60 +9,104 @@ local json = require "luci.jsonc"
local datatype = require "luci.cbi.datatypes"
local addr = arg[1]
if addr and not (datatype.hostname(addr) or datatype.ipaddr(addr)) then
os.exit(0)
end
local function fetch_connections(ip, port, passwd)
local raw = luci.sys.exec(string.format('curl -sL -m 3 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/connections', passwd, ip, port))
if not raw or raw == "" then
return nil
end
return json.parse(raw)
end
local function print_connection(c)
print("id: "..(c.id))
print("start: "..(c.start))
print("download: "..fs.filesize(c.download))
print("upload: "..fs.filesize(c.upload))
print("rule: "..(c.rule))
print("rulePayload: "..(c.rulePayload))
print("chains: ")
for o = 1, #(c.chains) do
print(" "..o..": "..(c.chains[o]))
end
print("metadata: ")
print(" sourceIP: "..(c.metadata.sourceIP))
print(" sourcePort: "..(c.metadata.sourcePort))
print(" host: "..(c.metadata.host == "" and "Empty" or c.metadata.host))
print(" destinationIP: "..(c.metadata.destinationIP))
print(" destinationPort: "..(c.metadata.destinationPort))
print(" network: "..(c.metadata.network))
print(" type: "..(c.metadata.type))
print("")
end
local function scan_connections(ip, port, passwd)
local info = fetch_connections(ip, port, passwd)
if not info then
return false
end
local found = false
for i = 1, #(info.connections) do
local c = info.connections[i]
local host = (c.metadata.host == "" and "Empty" or c.metadata.host)
if (datatype.hostname(addr) and string.lower(addr) == host) or (datatype.ipaddr(addr) and addr == c.metadata.destinationIP) then
found = true
print_connection(c)
end
end
return found
end
local function dump_connections(info)
local conn_lines = {}
for i = 1, #(info.connections) do
local c = info.connections[i]
local host = (c.metadata.host == "" and "Empty" or c.metadata.host)
conn_lines[#conn_lines + 1] = string.format("%d. SourceIP:【%s】 - Host:【%s】 - DestinationIP:【%s】 - Network:【%s】 - RulePayload:【%s】 - Lastchain:【%s】\n",
i,
tostring(c.metadata.sourceIP),
tostring(host),
tostring(c.metadata.destinationIP),
tostring(c.metadata.network),
tostring(c.rulePayload),
tostring(c.chains and c.chains[1]))
end
if #conn_lines > 0 then
local existing = fs.readfile("/tmp/openclash_debug.log") or ""
fs.writefile("/tmp/openclash_debug.log", existing .. table.concat(conn_lines))
end
end
local function debug_getcon()
local info, ip, host, diag_info
ip = fs.lanip()
local ip = fs.lanip(true)
local port = fs.uci_get_config("config", "cn_port")
local passwd = fs.uci_get_config("config", "dashboard_password") or ""
if ip and port then
info = luci.sys.exec(string.format('curl -sL -m 3 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/connections', passwd, ip, port))
if not ip or not port then
os.exit(0)
end
if not addr then
local info = fetch_connections(ip, port, passwd)
if info then
info = json.parse(info)
dump_connections(info)
end
if info then
local conn_lines = {}
for i = 1, #(info.connections) do
if info.connections[i].metadata.host == "" then
host = "Empty"
else
host = info.connections[i].metadata.host
end
if not addr then
conn_lines[#conn_lines + 1] = string.format("%d. SourceIP:【%s】 - Host:【%s】 - DestinationIP:【%s】 - Network:【%s】 - RulePayload:【%s】 - Lastchain:【%s】\n",
i,
tostring(info.connections[i].metadata.sourceIP),
tostring(host),
tostring(info.connections[i].metadata.destinationIP),
tostring(info.connections[i].metadata.network),
tostring(info.connections[i].rulePayload),
tostring(info.connections[i].chains and info.connections[i].chains[1]))
else
if datatype.hostname(addr) and string.lower(addr) == host or datatype.ipaddr(addr) and addr == (info.connections[i].metadata.destinationIP) then
print("id: "..(info.connections[i].id))
print("start: "..(info.connections[i].start))
print("download: "..fs.filesize(info.connections[i].download))
print("upload: "..fs.filesize(info.connections[i].upload))
print("rule: "..(info.connections[i].rule))
print("rulePayload: "..(info.connections[i].rulePayload))
print("chains: ")
for o = 1, #(info.connections[i].chains) do
print(" "..o..": "..(info.connections[i].chains[o]))
end
print("metadata: ")
print(" sourceIP: "..(info.connections[i].metadata.sourceIP))
print(" sourcePort: "..(info.connections[i].metadata.sourcePort))
print(" host: "..host)
print(" destinationIP: "..(info.connections[i].metadata.destinationIP))
print(" destinationPort: "..(info.connections[i].metadata.destinationPort))
print(" network: "..(info.connections[i].metadata.network))
print(" type: "..(info.connections[i].metadata.type))
print("")
end
end
end
if not addr and #conn_lines > 0 then
local existing = fs.readfile("/tmp/openclash_debug.log") or ""
fs.writefile("/tmp/openclash_debug.log", existing .. table.concat(conn_lines))
os.exit(0)
end
local found = scan_connections(ip, port, passwd)
if not found then
luci.sys.call(string.format('( (sleep 5) | nc %s 443 ) >/dev/null 2>&1 &', addr))
luci.sys.call(string.format('( (sleep 5) | openssl s_client -connect %s:443 -servername %s -quiet -no_ign_eof ) >/dev/null 2>&1 &', addr, addr))
local deadline = os.time() + 5
local polls = 0
while os.time() < deadline and not found and polls < 5 do
polls = polls + 1
found = scan_connections(ip, port, passwd)
if not found and polls < 5 then
luci.sys.call("sleep 1")
end
end
end
@@ -72,20 +72,20 @@ EOF
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
if [ "$DASH_NAME" == "Dashboard" ]; then
UNPACK_FILE_DIR="/usr/share/openclash/ui/dashboard/"
if [ "$DASH_TYPE" == "Official" ]; then
DOWNLOAD_PATH="https://codeload.github.com/ayanamist/clash-dashboard/zip/refs/heads/gh-pages"
if [ "$DASH_TYPE" == "Official" ]; then
DOWNLOAD_PATH="https://codeload.github.com/ayanamist/clash-dashboard/zip/refs/heads/gh-pages"
FILE_PATH_INCLUDE="clash-dashboard-gh-pages"
else
DOWNLOAD_PATH="https://codeload.github.com/MetaCubeX/Razord-meta/zip/refs/heads/gh-pages"
DOWNLOAD_PATH="https://codeload.github.com/MetaCubeX/Razord-meta/zip/refs/heads/gh-pages"
FILE_PATH_INCLUDE="Razord-meta-gh-pages"
fi
elif [ "$DASH_NAME" == "Yacd" ]; then
elif [ "$DASH_NAME" == "Yacd" ]; then
UNPACK_FILE_DIR="/usr/share/openclash/ui/yacd/"
if [ "$DASH_TYPE" == "Official" ]; then
DOWNLOAD_PATH="https://codeload.github.com/haishanh/yacd/zip/refs/heads/gh-pages"
if [ "$DASH_TYPE" == "Official" ]; then
DOWNLOAD_PATH="https://codeload.github.com/haishanh/yacd/zip/refs/heads/gh-pages"
FILE_PATH_INCLUDE="yacd-gh-pages"
else
DOWNLOAD_PATH="https://codeload.github.com/MetaCubeX/Yacd-meta/zip/refs/heads/gh-pages"
DOWNLOAD_PATH="https://codeload.github.com/MetaCubeX/Yacd-meta/zip/refs/heads/gh-pages"
FILE_PATH_INCLUDE="Yacd-meta-gh-pages"
fi
elif [ "$DASH_NAME" == "Zashboard" ]; then
@@ -94,9 +94,9 @@ EOF
FILE_PATH_INCLUDE="zashboard-gh-pages-cdn-fonts"
else
UNPACK_FILE_DIR="/usr/share/openclash/ui/metacubexd/"
DOWNLOAD_PATH="https://codeload.github.com/MetaCubeX/metacubexd/zip/refs/heads/gh-pages"
DOWNLOAD_PATH="https://codeload.github.com/MetaCubeX/metacubexd/zip/refs/heads/gh-pages"
FILE_PATH_INCLUDE="metacubexd-gh-pages"
fi
fi
TARGET_FILE_DIR="${UNPACK_FILE_DIR%/}"
TARGET_PARENT_DIR="$(dirname "$TARGET_FILE_DIR")"
NEW_FILE_DIR="${TARGET_PARENT_DIR}/.openclash_dashboard_new.$$"
@@ -9,7 +9,7 @@ GET_ETAG_TIMESTAMP_BY_PATH() {
[ ! -f "$ETAG_CACHE" ] && return 1
local path_hash=$(echo -n "$path" | md5sum | cut -d' ' -f1)
awk -v hash="$path_hash" '
$0 ~ "^\\[" hash "\\]" { found=1; next }
/^\[/ { found=0 }
@@ -23,7 +23,7 @@ GET_ETAG_BY_PATH() {
[ ! -f "$ETAG_CACHE" ] && return 1
local path_hash=$(echo -n "$path" | md5sum | cut -d' ' -f1)
awk -v hash="$path_hash" '
$0 ~ "^\\[" hash "\\]" { found=1; next }
/^\[/ { found=0 }
@@ -33,16 +33,16 @@ GET_ETAG_BY_PATH() {
# 保存或更新 ETag
SAVE_ETAG_TO_CACHE() {
local url="\"$1\""
local etag="\"$2\""
local path="\"$3\""
local url="\"$1\""
local etag="\"$2\""
local path="\"$3\""
local time="\"$(date '+%Y-%m-%d %H:%M:%S')\""
local path_hash=$(echo -n "$3" | md5sum | cut -d' ' -f1)
mkdir -p "$(dirname "$ETAG_CACHE")"
[ ! -f "$ETAG_CACHE" ] && echo "# ETag Cache File" > "$ETAG_CACHE"
if grep -q "^\[$path_hash\]" "$ETAG_CACHE"; then
local temp_file="${ETAG_CACHE}.tmp"
awk -v hash="$path_hash" \
@@ -50,27 +50,27 @@ SAVE_ETAG_TO_CACHE() {
-v new_etag="$etag" \
-v new_path="$path" \
-v new_time="$time" '
$0 ~ "^\\[" hash "\\]" {
print;
found=1;
next
$0 ~ "^\\[" hash "\\]" {
print;
found=1;
next
}
/^\[/ { found=0 }
found && /^url=/ {
print "url=" new_url;
next
found && /^url=/ {
print "url=" new_url;
next
}
found && /^path=/ {
print "path=" new_path;
next
found && /^path=/ {
print "path=" new_path;
next
}
found && /^etag=/ {
print "etag=" new_etag;
next
found && /^etag=/ {
print "etag=" new_etag;
next
}
found && /^time=/ {
print "time=" new_time;
next
found && /^time=/ {
print "time=" new_time;
next
}
{ print }
' "$ETAG_CACHE" > "$temp_file" && mv "$temp_file" "$ETAG_CACHE"
@@ -1,8 +1,8 @@
#!/usr/bin/lua
require "nixio"
require "luci.util"
require "luci.sys"
local util = require "luci.util"
local ntm = require "luci.model.network".init()
local cidr = require "luci.ip"
local fs = require "luci.openclash"
@@ -12,18 +12,62 @@ local wan, wan6
if not type then os.exit(0) end
if pcall(function() local x = ntm:get_all_wan_networks(); local y = ntm:get_all_wan6_networks(); end) then
wan = ntm:get_all_wan_networks()
wan6 = ntm:get_all_wan6_networks()
elseif pcall(function() local x = ntm:get_wan_networks(); local y = ntm:get_wan6_networks(); end) then
wan = ntm:get_wan_networks()
wan6 = ntm:get_wan6_networks()
elseif pcall(function() local x = ntm:get_wannet(); local y = ntm:get_wan6net(); end) then
wan = {}
wan6 = {}
wan[1] = ntm:get_wannet()
wan6[1] = ntm:get_wan6net()
else
local function get_all_wan_networks()
local nets = { }
local _, object
for _, object in ipairs(util.ubus()) do
local name = object:match("^network%.interface%.(.+)")
if name then
local stat = util.ubus(object, "status", { })
if stat and stat.route then
local rt
for _, rt in ipairs(stat.route) do
if not rt.table and rt.target == "0.0.0.0" and rt.mask == 0 then
local net = ntm:get_network(name)
if net then
nets[#nets+1] = net
end
break
end
end
end
end
end
return nets
end
local function get_all_wan6_networks()
local nets = { }
local _, object
for _, object in ipairs(util.ubus()) do
local name = object:match("^network%.interface%.(.+)")
if name then
local stat = util.ubus(object, "status", { })
if stat and stat.route then
local rt
for _, rt in ipairs(stat.route) do
if not rt.table and rt.target == "::" and rt.mask == 0 then
local net = ntm:get_network(name)
if net then
nets[#nets+1] = net
end
break
end
end
end
end
end
return nets
end
local ok = pcall(function() wan = get_all_wan_networks(); wan6 = get_all_wan6_networks(); end)
if not ok then
ok = pcall(function() wan = ntm:get_wan_networks(); wan6 = ntm:get_wan6_networks(); end)
end
if not ok then
ok = pcall(function() wan = { ntm:get_wannet() }; wan6 = { ntm:get_wan6net() }; end)
end
if not ok then
os.exit(0)
end
@@ -63,7 +107,7 @@ if type == "dns" then
if wan then
for o = 1, #(rv.wan) do
for i = 1, #(rv.wan[o].dns) do
if rv.wan[o].dns[i] ~= rv.wan[o].gwaddr and rv.wan[o].dns[i] ~= rv.wan[o].ipaddr then
if rv.wan[o].dns[i] ~= rv.wan[o].ipaddr then
print(rv.wan[o].dns[i])
end
end
@@ -75,7 +119,7 @@ if type == "dns6" then
if wan6 then
for o = 1, #(rv.wan6) do
for i = 1, #(rv.wan6[o].dns) do
if rv.wan6[o].dns[i] ~= rv.wan6[o].gw6addr and rv.wan6[o].ip6addr then
if rv.wan6[o].dns[i] ~= rv.wan6[o].ip6addr then
print(rv.wan6[o].dns[i])
end
end
@@ -0,0 +1,39 @@
#!/bin/sh
# Same-origin reverse proxy for the mihomo control API (https://<luci host>/oc-api/, see
# ocGetDashboardApiOrigin in common.js), an https page cannot open ws://. -n skips the
# reload for callers that reload nginx themselves.
[ -d /etc/nginx/conf.d ] || exit 0
PORT=$(uci -q get openclash.config.cn_port)
[ -n "$PORT" ] || PORT=9090
FILE=/etc/nginx/conf.d/openclash.locations
NEW=$(cat <<EOF
location /oc-api/ {
proxy_pass http://127.0.0.1:${PORT}/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
EOF
)
nginx -V 2>&1 | grep -q 'without-http_proxy_module' && NEW=""
if [ -f "$FILE" ] && [ "$(cat "$FILE")" = "$NEW" ]; then
exit 0
fi
if [ -n "$NEW" ]; then
printf '%s\n' "$NEW" > "$FILE"
else
rm -f "$FILE"
fi
[ "$1" = "-n" ] || /etc/init.d/nginx reload >/dev/null 2>&1
exit 0
@@ -4,35 +4,35 @@
JOB_COUNTER_FILE="/tmp/openclash_jobs"
unify_ps_status() {
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep -v grep |grep -c "$1")"
else
echo "$(ps -w |grep -v grep |grep -c "$1")"
fi
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep -v grep |grep -c "$1")"
else
echo "$(ps -w |grep -v grep |grep -c "$1")"
fi
}
unify_ps_pids() {
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep "$1" |grep -v grep |awk '{print $2}' 2>/dev/null)"
else
echo "$(ps -w |grep "$1" |grep -v grep |awk '{print $1}' 2>/dev/null)"
fi
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep "$1" |grep -v grep |awk '{print $2}' 2>/dev/null)"
else
echo "$(ps -w |grep "$1" |grep -v grep |awk '{print $1}' 2>/dev/null)"
fi
}
unify_ps_prevent() {
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep -v grep |grep -c "/etc/init.d/openclash")"
else
echo "$(ps -w |grep -v grep |grep -c "/etc/init.d/openclash")"
fi
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep -v grep |grep -c "/etc/init.d/openclash")"
else
echo "$(ps -w |grep -v grep |grep -c "/etc/init.d/openclash")"
fi
}
unify_ps_cfgname() {
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep /etc/openclash/clash 2>/dev/null |grep -v grep |awk -F '-f ' '{print $2}' 2>/dev/null)"
else
echo "$(ps -w |grep /etc/openclash/clash 2>/dev/null |grep -v grep |awk -F '-f ' '{print $2}' 2>/dev/null)"
fi
if [ "$(ps --version 2>&1 |grep -c procps-ng)" -eq 1 ]; then
echo "$(ps -efw |grep /etc/openclash/clash 2>/dev/null |grep -v grep |awk -F '-f ' '{print $2}' 2>/dev/null)"
else
echo "$(ps -w |grep /etc/openclash/clash 2>/dev/null |grep -v grep |awk -F '-f ' '{print $2}' 2>/dev/null)"
fi
}
inc_job_counter() {
File diff suppressed because it is too large. Load diff
@@ -1,219 +1,198 @@
#!/usr/bin/lua
-- Parse subscription files (plain or base64) and extract server addresses.
-- Prints one server address per node of a subscription file, the fallback of the watchdog
-- for provider files that are not YAML.
--
-- The share links are the ones mihomo accepts (common/convert/converter.go), they follow
-- the schemes of the clients themselves:
-- ss:// SIP002, with the plugin query, or the legacy fully encoded form
-- ssr:// base64 of host:port:protocol:method:obfs:password/?params
-- vmess:// V2RayN base64 json, or the Xray VMessAEAD uri (XTLS/Xray-core#716)
-- vless:// Xray uri, the host of the uri may be base64 encoded
-- trojan:// password as userinfo
-- hysteria:// github.com/apernet/hysteria (docs/developers/URI-Scheme)
-- hysteria2:// and hy2://, also with the +realm suffix and with port hopping
-- tuic:// temporary unofficial standard, uuid:password or the v5 token as userinfo
-- anytls:// github.com/anytls/anytls-go (docs/uri_scheme.md)
-- mierus:// one uri per profile, the port and protocol query keys are repeated
-- socks5://, socks5h://, http://, https://
--
-- Only the server address is needed, every other part of the uri is ignored.
local nixio = require "nixio"
local jsonc = require "luci.jsonc"
local util = require "luci.util"
local fs = require "luci.openclash"
local jsonc = require "luci.jsonc"
if not nixio.fs.access("/usr/bin/base64") and not nixio.fs.access("/bin/base64") then
os.exit(1)
local BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local BASE64_PAD = 61
-- Decodes base64 in every form a subscription may use: standard or url safe, padded or not.
-- A string that is not base64 at all is not a link payload, the caller gets nil.
local function decode64(text)
if not text then
return nil
end
text = text:gsub("[%s]", ""):gsub("%-", "+"):gsub("_", "/"):gsub("=+$", "")
if text == "" or #text % 4 == 1 or not text:match("^[A-Za-z0-9+/]+$") then
return nil
end
text = text .. ("===="):sub(1, (4 - #text % 4) % 4)
local result = {}
for group in text:gmatch("....") do
local c3, c4 = group:byte(3, 4)
local i1 = BASE64:find(group:sub(1, 1), 1, true)
local i2 = BASE64:find(group:sub(2, 2), 1, true)
local i3 = c3 == BASE64_PAD and 0 or BASE64:find(group:sub(3, 3), 1, true)
local i4 = c4 == BASE64_PAD and 0 or BASE64:find(group:sub(4, 4), 1, true)
if not (i1 and i2 and i3 and i4) then
return nil
end
i1, i2, i3, i4 = i1 - 1, i2 - 1, i3 - 1, i4 - 1
result[#result + 1] = string.char(i1 * 4 + math.floor(i2 / 16))
if c3 ~= BASE64_PAD then
result[#result + 1] = string.char((i2 % 16) * 16 + math.floor(i3 / 4))
end
if c4 ~= BASE64_PAD then
result[#result + 1] = string.char((i3 % 4) * 64 + i4)
end
end
return table.concat(result)
end
local file_path = arg[1]
if not file_path or not nixio.fs.access(file_path) then
os.exit(1)
-- scheme, userinfo, host, port, query and fragment of a uri. A host without a port and an
-- IPv6 host in brackets are accepted, the port of a hop link is a list of ports.
local function parse_uri(uri)
local scheme, body = uri:match("^([%w%+%-%.]+)://(.*)$")
if not scheme then
return nil
end
local authority = body:match("^([^/%?#]*)") or ""
local remainder = body:sub(#authority + 1)
local userinfo, hostport = authority:match("^(.*)@([^@]+)$")
if not hostport then
userinfo, hostport = nil, authority
end
local host, port
if hostport:match("^%[") then
host, port = hostport:match("^%[([^%]]+)%]:?([%d,%-]*)$")
else
host, port = hostport:match("^([^:]*):([%d,%-]*)$")
end
return {
scheme = scheme:lower(),
userinfo = userinfo,
host = host or hostport,
port = port,
path = remainder:match("^[^%?#]*"),
query = remainder:match("%?([^#]*)"),
fragment = remainder:match("#(.*)$")
}
end
local function base64_decode_subscription(str)
local result = fs.decode64(str)
if result and result:find("://") then
return result
end
return nil
-- mihomo decodes the host of a vless uri when it is base64 encoded, the watchdog can only
-- use it when the result looks like an address
local function decoded_host(host)
local decoded = decode64(host)
if decoded and decoded:match("^[%w%.%-%_]+$") and decoded:find("%.") then
return decoded
end
return host
end
local function url_decode(str)
return str:gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
local function without_brackets(host)
if not host then
return nil
end
return host:match("^%[(.-)%]$") or host
end
local function parse_query_params(query_string)
local params = {}
if not query_string then return params end
-- The server address of one share link, nil when the line is not a link the plugin knows
local function server_of(uri)
local parsed = parse_uri(uri)
if not parsed then
return nil
end
local scheme = parsed.scheme
local host = parsed.host
for param in query_string:gmatch("([^&]+)") do
local key, value = param:match("([^=]*)=?(.*)")
if key then
params[key] = url_decode(value or "")
end
end
return params
if scheme == "vmess" then
-- V2RayN: the whole link body is the base64 of a json document
local decoded = decode64((uri:match("^%w+://([^#]+)") or ""))
if decoded and decoded:sub(1, 1) == "{" then
local ok, node = pcall(jsonc.parse, decoded)
if ok and type(node) == "table" and type(node.add) == "string" and node.add ~= "" then
return without_brackets(node.add)
end
return nil
end
-- Xray VMessAEAD: uuid@host:port
return host
end
if scheme == "ss" then
if parsed.port and parsed.port ~= "" then
return host
end
-- the legacy link is the base64 of method:password@host:port
local decoded = decode64(host)
if decoded then
return decoded:match("@%[([^%]]+)%]") or decoded:match("@([^:/]+)")
end
return nil
end
if scheme == "ssr" then
local decoded = decode64((uri:match("^%w+://([^#]+)") or ""))
if not decoded then
return nil
end
return decoded:match("^([^:]+):")
end
if scheme == "hysteria2" or scheme == "hy2" or scheme == "hysteria2+realm" or scheme == "hy2+realm" then
return without_brackets(host)
end
if scheme == "vless" then
return decoded_host(host)
end
if scheme == "hysteria" or scheme == "tuic" or scheme == "trojan" or scheme == "mierus"
or scheme == "anytls" or scheme == "socks" or scheme == "socks5" or scheme == "socks5h"
or scheme == "http" or scheme == "https" then
return without_brackets(host)
end
return nil
end
local function parse_url(url_str)
local scheme, rest = url_str:match("^([%w%-]+)://(.+)")
if not scheme then return nil end
local userinfo, host_part = rest:match("^([^@]+)@(.+)")
if not userinfo then
userinfo = ""
host_part = rest
end
local path_query_fragment = host_part:match("^[^/]*(.*)") or ""
local host_port = host_part:match("^([^/]*)")
local host, port = host_port:match("^%[([^%]]+)%]:(%d+)") -- IPv6
if not host then
host, port = host_port:match("^([^:]+):(%d+)")
end
if not host then
host = host_port
port = nil
end
local path, query_fragment = path_query_fragment:match("^([^?]*)(.*)")
path = path or ""
local query, fragment
if query_fragment then
if query_fragment:sub(1,1) == "?" then
query, fragment = query_fragment:sub(2):match("^([^#]*)(.*)")
if fragment and fragment:sub(1,1) == "#" then
fragment = fragment:sub(2)
end
elseif query_fragment:sub(1,1) == "#" then
fragment = query_fragment:sub(2)
end
end
local username, password = userinfo:match("^([^:]*):?(.*)")
return {
scheme = scheme:lower(),
username = username or "",
password = password or "",
host = host,
port = port,
path = path,
query = query,
fragment = fragment
}
-- A provider file that is a broken yaml still names the address of its nodes
local function server_of_yaml(line)
local value = line:match("^%s*server%s*:%s*([^%s#%]]+)")
if not value then
return nil
end
return value:gsub("^[\"']", ""):gsub("[\"']$", "")
end
local function get_server_from_url(line)
line = util.trim(line)
local scheme, original_body = line:match("^([%w%-]+)://(.+)")
if not scheme or not original_body then return nil end
local path = arg[1]
local file = path and io.open(path, "r")
if not file then
os.exit(1)
end
local content = file:read("*a")
file:close()
local server = nil
scheme = scheme:lower()
if scheme == "vmess" then
-- fragment
original_body = original_body:match("([^#]+)") or original_body
-- base64
local decoded = fs.decode64(original_body)
if decoded then
-- V2RayN JSON
local ok, data = pcall(jsonc.parse, decoded)
if ok and type(data) == "table" and data.add and data.add ~= "" then
server = data.add
end
else
-- Xray VMessAEAD
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
end
elseif scheme == "vless" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "trojan" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "ss" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
else
-- ss://base64
original_body = original_body:match("([^#]+)") or original_body
local decoded = fs.decode64(original_body)
if decoded then
server = decoded:match("@([^:/]+)")
else
server = original_body:match("[^@]+@([^:/]+)")
end
end
elseif scheme == "ssr" then
original_body = original_body:match("([^#]+)") or original_body
local decoded = fs.decode64(original_body)
if decoded then
-- ssr://host:port:protocol:method:obfs:urlsafebase64pass/?params
local before_query = decoded:match("^([^/?]+)")
if before_query then
local parts = {}
for part in before_query:gmatch("([^:]+)") do
table.insert(parts, part)
end
-- host:port:protocol:method:obfs:password
if #parts == 6 then
server = parts[1]
end
end
end
elseif scheme == "hysteria" or scheme == "hysteria2" or scheme == "hy2" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "tuic" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "socks" or scheme == "socks5" or scheme == "socks5h" or
scheme == "http" or scheme == "https" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
elseif scheme == "anytls" then
local url_parts = parse_url(line)
if url_parts and url_parts.host then
server = url_parts.host
end
end
-- fallback
if not server then
local body_to_parse = fs.decode64(original_body) or original_body
server = body_to_parse:match("@([^:/]+)") or -- user@host
body_to_parse:match("([^:/]+)") or -- host
body_to_parse:match("//([^:/]+)") -- //host
end
return server
local decoded = decode64(content:gsub("%s+", ""))
if decoded and decoded:find("://", 1, true) then
content = decoded
end
local f = io.open(file_path, "r")
if not f then os.exit(1) end
local content = f:read("*a")
f:close()
local decoded_content = base64_decode_subscription(content:gsub("%s+", ""))
if decoded_content then
content = decoded_content
for line in content:gmatch("[^\r\n]+") do
local text = util.trim(line)
local ok, server = pcall(server_of, text)
if not ok or not server or server == "" then
server = server_of_yaml(text)
end
if server and server ~= "" then
print(server)
end
end
for line in content:gmatch("([^\r\n]+)") do
local server = get_server_from_url(line)
if server and util.trim(server) ~= "" then
print(util.trim(server))
end
end
@@ -25,6 +25,12 @@ inc_job_counter
RELEASE_BRANCH=$(uci_get_config "release_branch" || echo "master")
if [ -x "/usr/bin/apk" ]; then
PKG_TYPE="apk"
else
PKG_TYPE="opkg"
fi
PLUGIN_DIRECT=0
if [ -n "$3" ] && echo "$3" | grep -qE '^https?://'; then
PLUGIN_DIRECT=1
@@ -77,9 +83,9 @@ run_with_timeout() {
}
LAST_VER=$(echo "$PLUGIN_LATEST" |sed "s/^v//g" |tr -d "\n")
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
OP_CV=$(rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
else
OP_CV=$(rm -f /lib/apk/db/lock && apk list luci-app-openclash 2>/dev/null|grep "installed" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
fi
OP_LV="$LAST_VER"
@@ -125,10 +131,10 @@ fi
if [ "$PLUGIN_DIRECT" -eq 1 ]; then
# Direct download URL (historical version / downgrade) — skip version compare
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
DOWNLOAD_URL="$3"
DOWNLOAD_PATH="/tmp/openclash.ipk"
elif [ -x "/usr/bin/apk" ]; then
else
DOWNLOAD_URL="$3"
DOWNLOAD_PATH="/tmp/openclash.apk"
fi
@@ -138,27 +144,27 @@ elif [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" &&
LOG_TIP "Start downloading【OpenClash - v$LAST_VER】..."
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
DOWNLOAD_URL="${github_address_mod}gh/vernesong/OpenClash@package/${RELEASE_BRANCH}/luci-app-openclash_${LAST_VER}_all.ipk"
DOWNLOAD_PATH="/tmp/openclash.ipk"
elif [ -x "/usr/bin/apk" ]; then
else
DOWNLOAD_URL="${github_address_mod}gh/vernesong/OpenClash@package/${RELEASE_BRANCH}/luci-app-openclash-${LAST_VER}.apk"
DOWNLOAD_PATH="/tmp/openclash.apk"
fi
else
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/vernesong/OpenClash/package/${RELEASE_BRANCH}/luci-app-openclash_${LAST_VER}_all.ipk"
DOWNLOAD_PATH="/tmp/openclash.ipk"
elif [ -x "/usr/bin/apk" ]; then
else
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/vernesong/OpenClash/package/${RELEASE_BRANCH}/luci-app-openclash-${LAST_VER}.apk"
DOWNLOAD_PATH="/tmp/openclash.apk"
fi
fi
else
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
DOWNLOAD_URL="https://raw.githubusercontent.com/vernesong/OpenClash/package/${RELEASE_BRANCH}/luci-app-openclash_${LAST_VER}_all.ipk"
DOWNLOAD_PATH="/tmp/openclash.ipk"
elif [ -x "/usr/bin/apk" ]; then
else
DOWNLOAD_URL="https://raw.githubusercontent.com/vernesong/OpenClash/package/${RELEASE_BRANCH}/luci-app-openclash-${LAST_VER}.apk"
DOWNLOAD_PATH="/tmp/openclash.apk"
fi
@@ -194,7 +200,7 @@ if [ -n "$DOWNLOAD_URL" ]; then
pre_test_success=false
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
update_retry=0
max_update_retry=2
while [ $update_retry -lt $max_update_retry ]; do
@@ -220,7 +226,7 @@ if [ -n "$DOWNLOAD_URL" ]; then
LOG_ERROR "【$retry_count/$max_retries】【OpenClash - v$LAST_VER】Pre update test failed:【$(echo "$opkg_test_err" | tr '\n' ' ' | head -c 500)】..."
fi
fi
elif [ -x "/usr/bin/apk" ]; then
else
update_retry=0
max_update_retry=2
while [ $update_retry -lt $max_update_retry ]; do
@@ -257,9 +263,9 @@ if [ -n "$DOWNLOAD_URL" ]; then
sleep 2
continue
else
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
LOG_ERROR "【OpenClash - v$LAST_VER】Pre update test failed after 3 attempts, the file is saved in /tmp/openclash.ipk, please try to update manually with【opkg --force-downgrade install /tmp/openclash.ipk】"
elif [ -x "/usr/bin/apk" ]; then
else
LOG_ERROR "【OpenClash - v$LAST_VER】Pre update test failed after 3 attempts, the file is saved in /tmp/openclash.apk, please try to update manually with【apk add -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk】"
fi
early_exit
@@ -283,6 +289,12 @@ if [ -n "$DOWNLOAD_URL" ]; then
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_ps.sh
if [ -x "/usr/bin/apk" ]; then
PKG_TYPE="apk"
else
PKG_TYPE="opkg"
fi
UPDATE_LOCK="/tmp/lock/openclash_update_install.lock"
mkdir -p /tmp/lock
@@ -308,9 +320,9 @@ check_install_success()
local target_version="$1"
local current_version=""
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
current_version=$(rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
else
current_version=$(apk list luci-app-openclash 2>/dev/null |grep "installed" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
fi
@@ -321,6 +333,22 @@ check_install_success()
fi
}
openclash_service_running() {
ubus call service list '{"name":"openclash"}' 2>/dev/null | jsonfilter -e '@.openclash.instances.*.running' 2>/dev/null | grep -q 'true'
}
stop_openclash_before_install() {
if ! openclash_service_running; then
return 0
fi
/etc/init.d/openclash stop >/dev/null 2>&1
local i
for i in $(seq 1 30); do
openclash_service_running || return 0
sleep 1
done
}
install_missing_packages() {
local installed_before="$1"
@@ -328,11 +356,11 @@ install_missing_packages() {
for pkg in $installed_before; do
local retry_count=0
local max_retries=3
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
if opkg status "$pkg" >/dev/null 2>&1; then
continue
fi
elif [ -x "/usr/bin/apk" ]; then
else
if apk list "$pkg" |grep "installed" >/dev/null 2>&1; then
continue
fi
@@ -342,9 +370,9 @@ install_missing_packages() {
while [ $retry_count -lt $max_retries ]; do
retry_count=$((retry_count + 1))
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
pkg_err=$(opkg install "$pkg" 2>&1)
elif [ -x "/usr/bin/apk" ]; then
else
pkg_err=$(apk add "$pkg" 2>&1)
fi
if [ $? -eq 0 ]; then
@@ -365,13 +393,15 @@ install_retry_count=0
max_install_retries=3
install_success=false
stop_openclash_before_install
while [ $install_retry_count -lt $max_install_retries ]; do
install_retry_count=$((install_retry_count + 1))
LOG_TIP "【$install_retry_count/$max_install_retries】Installing the new version, please do not refresh the page or do other operations..."
packages_to_check="luci-compat kmod-inet-diag kmod-nft-tproxy kmod-ipt-nat iptables-mod-tproxy iptables-mod-extra ipset"
installed_before=""
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
for pkg in $packages_to_check; do
if opkg status "$pkg" >/dev/null 2>&1; then
installed_before="$installed_before $pkg"
@@ -379,7 +409,7 @@ while [ $install_retry_count -lt $max_install_retries ]; do
done
opkg --force-downgrade install /tmp/openclash.ipk >/dev/null 2>&1
install_err=$(cat /tmp/openclash_install.log 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
else
for pkg in $packages_to_check; do
if apk list "$pkg" |grep "installed" >/dev/null 2>&1; then
installed_before="$installed_before $pkg"
@@ -404,19 +434,26 @@ while [ $install_retry_count -lt $max_install_retries ]; do
done
if [ "$install_success" = true ]; then
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
rm -rf /tmp/openclash.ipk >/dev/null 2>&1
elif [ -x "/usr/bin/apk" ]; then
else
rm -rf /tmp/openclash.apk >/dev/null 2>&1
fi
else
if [ -x "/bin/opkg" ]; then
if [ "$PKG_TYPE" = "opkg" ]; then
LOG_ERROR "OpenClash update failed after 3 attempts, the file is saved in /tmp/openclash.ipk, please try to update manually with【opkg --force-downgrade install /tmp/openclash.ipk】【$(echo "$install_err" | tr '\n' ' ' | head -c 500)】"
elif [ -x "/usr/bin/apk" ]; then
else
LOG_ERROR "OpenClash update failed after 3 attempts, the file is saved in /tmp/openclash.apk, please try to update manually with【apk add -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk】【$(echo "$install_err" | tr '\n' ' ' | head -c 500)】"
fi
fi
dec_job_counter_and_restart "0"
restart_flag=0
if [ "$(uci -q get openclash.config.enable)" = "1" ]; then
if ! openclash_service_running; then
restart_flag=1
fi
fi
dec_job_counter_and_restart "$restart_flag"
del_update_lock
EOF
chmod 4755 /tmp/openclash_update.sh
@@ -7,9 +7,6 @@ local json = require "luci.jsonc"
local M = {}
local VERSION_CACHE_FILE = "/tmp/openclash_version_history.json"
local CDN_CACHE_FILE = "/tmp/openclash_cdn_info.json"
local function trim(s)
if not s then return "" end
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
@@ -19,7 +16,7 @@ function M.is_valid_version(s)
if not s or s == "" then return false end
s = trim(s)
if s == "" then return false end
if s:match("^<") then return false end
if s:match("^<") or s:match("%s") or s:match(":") then return false end
return true
end
@@ -42,44 +39,38 @@ function try_read(fd, maxlen)
return nil
end
local function read_json(path)
if not fs.access(path) then return nil end
local raw = fs.readfile(path)
if not raw or raw == "" then return nil end
local ok, parsed = pcall(json.parse, raw)
if ok and parsed and type(parsed) == "table" then
return parsed
end
return nil
end
local read_version_cache = fs.read_version_cache
local update_version_cache = fs.update_version_cache
local function write_json(path, data)
local tmp = path .. ".tmp"
fs.writefile(tmp, json.stringify(data))
os.rename(tmp, path)
end
M.read_version_cache = read_version_cache
M.update_version_cache = update_version_cache
local function read_version_cache()
return read_json(VERSION_CACHE_FILE)
end
local function update_version_cache(updater)
local parsed = read_version_cache() or {}
updater(parsed)
write_json(VERSION_CACHE_FILE, parsed)
end
local DEFAULT_CDN_LIST = {
"https://ghfast.top/",
"https://github.dpik.top/",
"https://gh-proxy.com/",
"https://git.yylx.win/"
}
local CDN_LIST_FILE = "/usr/share/openclash/res/cdn.list"
local CDN_LIST_REMOTE_URL = "https://raw.githubusercontent.com/vernesong/OpenClash/dev/luci-app-openclash/root/usr/share/openclash/res/cdn.list"
local CDN_LIST_MAX_AGE = 7 * 24 * 3600
local function cdn_list()
local list = fs.cdn_list()
if #list > 0 then return list end
return DEFAULT_CDN_LIST
return fs.cdn_list()
end
function M.update_cdn_list(force)
local mt = fs.mtime(CDN_LIST_FILE)
if not force and mt and (os.time() - mt) < CDN_LIST_MAX_AGE then
return false, "fresh"
end
local child = nixio.fork()
if child > 0 then
return true, "started"
elseif child == 0 then
local tmp = CDN_LIST_FILE .. ".tmp"
local cmd = string.format(
'curl -sL -m 10 --connect-timeout 5 "%s" -o "%s" && grep -q "https://" "%s" && mv "%s" "%s"',
CDN_LIST_REMOTE_URL, tmp, tmp, tmp, CDN_LIST_FILE)
nixio.exec("/bin/sh", "-c", cmd)
return nil, "exec_failed"
else
return false, "fork_error"
end
end
local function raw_url(path)
@@ -312,6 +303,11 @@ local function launch_file_batch(job)
end
end
local function is_version_response(buf)
if not buf or buf == "" then return false end
return M.is_valid_version(trim(buf:match("^[^\n\r]*") or ""))
end
local function collect_fork_results(jobs, max_jobs)
local results = {}
local active = 0
@@ -355,7 +351,7 @@ local function collect_fork_results(jobs, max_jobs)
end
pcall(child.fdi.close, child.fdi)
child.done = true
if winner == "" and child.buf ~= "" then
if winner == "" and child.buf ~= "" and is_version_response(child.buf) then
winner = child.buf
end
end
@@ -462,6 +458,7 @@ end
function M.fetch_version_history(branch, force, cdn, latest_only)
local result = { plugin = {}, core_meta = {}, core_smart = {}, latest = nil, error = nil, oix_ver = "" }
M.update_cdn_list()
local cur_oix = M.is_oix_mode()
local skip_core = cur_oix
local github_address_mod = fs.uci_get_config("config", "github_address_mod") or "0"
@@ -15,11 +15,39 @@ STREAM_AUTO_SELECT=0
FIREWALL_RELOAD=0
MAX_FIREWALL_RELOAD=3
FW4=$(command -v fw4)
PROXY_FWMARK="0x162"
## Skip Proxies Address
skip_proxies_address()
{
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
REG4 = /^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/
REG6 = /^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))|\[(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))\]$/i
REG_DOMAIN = /([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})/
def write_ips_set(ips)
return if ips.nil? or ips.empty?
firewall_v = '$FW4'.empty? ? 'ipt' : 'nft'
set_commands = []
ips.each do |ip|
next if ip.nil? or ip.empty?
if ip.match(REG4) then
if firewall_v == 'nft' then
set_commands << 'nft add element inet fw4 localnetwork { \"' + ip + '\" } 2>/dev/null'
else
set_commands << 'ipset add localnetwork \"' + ip + '\" 2>/dev/null'
end
elsif ip.match(REG6) then
if firewall_v == 'nft' then
set_commands << 'nft add element inet fw4 localnetwork6 { \"' + ip + '\" } 2>/dev/null'
else
set_commands << 'ipset add localnetwork6 \"' + ip + '\" 2>/dev/null'
end
end
end
system(set_commands.join('; ')) if not set_commands.empty?
end
begin
Value = YAML.load_file('$CONFIG_FILE');
rescue Exception => e
@@ -103,14 +131,11 @@ begin
domains_to_resolve = Array.new
ips = Array.new
reg_domain = /([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})/
reg4 = /^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/
reg6 = /^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))|\[(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))\]$/i
servers_to_process.each do |server|
if server.to_s.match(reg4) or server.to_s.match(reg6)
if server.to_s.match(REG4) or server.to_s.match(REG6)
ips.push(server)
elsif server.to_s.match(reg_domain)
elsif server.to_s.match(REG_DOMAIN)
domains_to_resolve.push(server)
end
end
@@ -119,18 +144,33 @@ begin
domains_to_resolve.uniq!
if not domains_to_resolve.empty?
host = '127.0.0.1'
port = IO.popen('uci -q get openclash.config.cn_port').read.strip
pw = IO.popen('uci -q get openclash.config.dashboard_password').read.strip
ips_mutex = Mutex.new
queue = Queue.new
domains_to_resolve.each{|d| queue << d}
domains_to_resolve.each do |d|
queue << [d, 'A']
queue << [d, 'AAAA']
end
threads = (1..[10, queue.size].min).map do
Thread.new do
while domain = queue.pop(true) rescue nil
syscall = '/usr/share/openclash/openclash_debug_dns.lua 2>/dev/null \"' + domain + '\" \"true\"'
while task = (queue.pop(true) rescue nil)
domain, qtype = task
syscall = 'curl -s -m 3 -H \"Authorization: Bearer ' + pw + '\" \"http://' + host + ':' + port + '/dns/query?name=' + domain + '&type=' + qtype + '\" | jsonfilter -e \"@.Answer[*].data\" 2>/dev/null'
result = IO.popen(syscall).read.split(/\n+/)
if result
ips_mutex.synchronize do
result.each{|ip| ips.push(ip)}
result.each do |ip|
next if ip.nil? or ip.empty?
if qtype == 'AAAA'
ips.push(ip) if ip.match(REG6)
else
ips.push(ip) if ip.match(REG4)
end
end
end
end
end
@@ -142,28 +182,7 @@ begin
ips.compact!
ips.uniq!
# Add IPs to ipset/nft
if not ips.empty? then
firewall_v = '$FW4'.empty? ? 'ipt' : 'nft'
set_commands = []
ips.each do |ip|
next if ip.nil? or ip.empty?
if ip.match(reg4) then
if firewall_v == 'nft' then
set_commands << 'nft add element inet fw4 localnetwork { \"' + ip + '\" } 2>/dev/null'
else
set_commands << 'ipset add localnetwork \"' + ip + '\" 2>/dev/null'
end
elsif ip.match(reg6) then
if firewall_v == 'nft' then
set_commands << 'nft add element inet fw4 localnetwork6 { \"' + ip + '\" } 2>/dev/null'
else
set_commands << 'ipset add localnetwork6 \"' + ip + '\" 2>/dev/null'
end
end
end
system(set_commands.join('; ')) if not set_commands.empty?
end
write_ips_set(ips)
rescue Exception => e
YAML.LOG_ERROR('Set Proxies Address Skip: Failed【' + e.message + '】');
end" 2>/dev/null >> $LOG_FILE
@@ -199,6 +218,9 @@ do
stream_auto_select_openai=$(uci_get_config "stream_auto_select_openai" || echo 0)
stream_auto_select_claude=$(uci_get_config "stream_auto_select_claude" || echo 0)
stream_auto_select_gemini=$(uci_get_config "stream_auto_select_gemini" || echo 0)
stream_auto_select_bahamut=$(uci_get_config "stream_auto_select_bahamut" || echo 0)
stream_auto_select_spotify=$(uci_get_config "stream_auto_select_spotify" || echo 0)
stream_auto_select_steam=$(uci_get_config "stream_auto_select_steam" || echo 0)
upnp_lease_file=$(uci -q get upnpd.config.upnp_lease_file)
#wait for core start complete
@@ -208,11 +230,21 @@ do
done >/dev/null 2>&1
#check the clash service status
if ! ubus call service list '{"name":"openclash"}' 2>/dev/null | jsonfilter -e '@.openclash.instances.*.running' | grep -q 'true'; then
uci -q set openclash.config.enable=0
uci -q commit openclash
/etc/init.d/openclash stop >/dev/null 2>&1
exit 0
CORE_JSON=$(ubus call service list '{"name":"openclash"}' 2>/dev/null)
if [ -n "$CORE_JSON" ]; then
# stop signals the watchdog itself, this exits it when the core vanished otherwise
if [ -z "$(echo "$CORE_JSON" | jsonfilter -e '@.openclash.instances.openclash')" ]; then
LOG_WATCHDOG "OpenClash Service Not Running, Exit..."
exit 0
fi
# procd drops the respawn table once it gave up on the core (crash loop)
if [ "$(echo "$CORE_JSON" | jsonfilter -e '@.openclash.instances.openclash.running')" != "true" ] \
&& [ -z "$(echo "$CORE_JSON" | jsonfilter -e '@.openclash.instances.openclash.respawn.retry')" ]; then
uci -q set openclash.config.enable=0
uci -q commit openclash
/etc/init.d/openclash stop >/dev/null 2>&1
exit 0
fi
fi
## Porxy history
@@ -244,7 +276,7 @@ fi
LOG_WATCHDOG "Setting Firewall For Rules Order..."
/etc/init.d/openclash reload "firewall"
let FIREWALL_RELOAD++
elif [ -n "$(ip link show utun 2>/dev/null)" ] && [ -z "$(ip route list table 354)" ]; then
elif [ -n "$(ip link show utun 2>/dev/null)" ] && [ -n "$(ip rule show 2>/dev/null | grep "fwmark $PROXY_FWMARK")" ] && [ -z "$(ip route list table 354)" ]; then
## 路由表检查
LOG_WATCHDOG "Setting Firewall For IP Rules Table Recreate..."
/etc/init.d/openclash reload "firewall"
@@ -385,7 +417,7 @@ fi
uci -q delete dhcp.@dnsmasq[0].resolvfile
uci -q set dhcp.@dnsmasq[0].noresolv=1
[ "$disable_masq_cache" -eq 1 ] && {
uci -q set dhcp.@dnsmasq[0].cachesize=0
uci -q set dhcp.@dnsmasq[0].cachesize=0
}
uci -q commit dhcp
/etc/init.d/dnsmasq restart >/dev/null 2>&1
@@ -463,6 +495,18 @@ fi
LOG_INFO "【Gemini】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Gemini" >> $LOG_FILE
fi
if [ "$stream_auto_select_bahamut" -eq 1 ]; then
LOG_INFO "【Bahamut Anime】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Bahamut Anime" >> $LOG_FILE
fi
if [ "$stream_auto_select_spotify" -eq 1 ]; then
LOG_INFO "【Spotify】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Spotify" >> $LOG_FILE
fi
if [ "$stream_auto_select_steam" -eq 1 ]; then
LOG_INFO "【Steam】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Steam" >> $LOG_FILE
fi
fi
fi
STREAM_AUTO_SELECT=$(expr "$STREAM_AUTO_SELECT" + 1)
@@ -1,13 +1,15 @@
# CDN proxy address list (one URL per line, "#" starts a comment)
# Comment out a line with "#" to temporarily disable it
https://ghfast.top/
https://git.yylx.win/
https://cdn.akaere.online/
# Atom-feed-capable proxies
https://github.chenc.dev/
https://g.z321.cc.cd/
https://ghp.keleyaa.com/
https://xsadwsd.kdns.fr/
https://777.z321.cc.cd/
# Raw/Release download proxies
https://gh-proxy.com/
https://gh.jasonzeng.dev/
https://ghproxy.monkeyray.net/
https://down.mxw.xx.kg/
https://github.tbap.top/
https://ghm.078465.xyz/
https://gh.monlor.com/
https://gh.idayer.com/
https://git.yylx.win/
https://gh.acmsz.top/
https://gh.jjj.gv.uy/
https://g.blfrp.cn/
https://ghfast.top/
@@ -136,7 +136,7 @@ profile: # 存储 select 选择记录
# Tun 配置
tun:
enable: false
stack: system # gvisor/mixed
stack: system # gvisor/mixed/mips
dns-hijack:
- 0.0.0.0:53 # 需要劫持的 DNS
# auto-detect-interface: true # 自动识别出口网卡
@@ -214,7 +214,7 @@ sniffer:
HTTP: # 需要嗅探的端口
ports: [80, 8080-8880]
# 可覆盖 sniffer.override-destination
override-destination: true
override-destination: false
force-domain:
- +.v2ex.com
# skip-src-address: # 对于来源ip跳过嗅探
@@ -850,7 +850,7 @@ proxies: # socks5
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
@@ -1373,7 +1373,7 @@ proxies: # socks5
- name: sudoku
type: sudoku
server: server_ip/domain # 1.2.3.4 or domain
port: 443
port: 443
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;none 不提供 AEAD 保护)
padding-min: 2 # 最小填充率(0-100)
@@ -2211,7 +2211,7 @@ listeners:
type: tun
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
stack: system # gvisor / mixed
stack: system # gvisor / mixed / mips
dns-hijack:
- 0.0.0.0:53 # 需要劫持的 DNS
# auto-detect-interface: false # 自动识别出口网卡
@@ -1,6 +1,5 @@
默认(附带用于Clash的AdGuard DNS),default_with_clash_adg.yml,https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/default_with_clash_adg.yml
无Urltest,no_urltest.ini,https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/no_urltest.ini
带Urltest,urltest.ini,https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/urltest.ini
无Urltest,no_urltest.ini,https://gist.githubusercontent.com/tindy2013/1fa08640a9088ac8652dbd40c5d2715b/raw/no_urltest.ini
ACL4SSR 规则标准版,ACL4SSR.ini,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR.ini
ACL4SSR 规则 AdblockPlus,ACL4SSR_AdblockPlus.ini,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_AdblockPlus.ini
ACL4SSR 规则 BackCN,ACL4SSR_BackCN.ini,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_BackCN.ini
@@ -1,298 +1,117 @@
#!/bin/sh
write_ruby_part()
#The custom overwrite script never edits the config itself, its helper calls are recorded here
#and executed by YAML.overwrite_run_custom() once it finished; the argument counts must stay in
#sync with OVERWRITE_HELPERS in YAML.rb
ruby_record()
{
local part="$1" sid="${OPENCLASH_OVERWRITE_SID:-unknown}"
if [ -z "$part" ]; then
return
fi
if [ "$OVERWRITE_PARENT" = "yaml_overwrite" ]; then
mkdir -p /tmp/yaml_openclash_ruby_parts 2>/dev/null
echo "threads << Thread.new do begin $part; rescue Exception => e; YAML.LOG_ERROR('Set Custom Overwrite Script Failed,【%s】' % [e.message]); end; end" >> "/tmp/yaml_openclash_ruby_parts/$sid"
else
echo "threads << Thread.new do begin $part; rescue Exception => e; YAML.LOG_ERROR('Set Custom Overwrite Script Failed,【%s】' % [e.message]); end; end" >> /tmp/yaml_openclash_ruby_parse
fi
}
local op="$1" arity=0 count=0 arg
shift
run_ruby_part()
{
local part="$1" show_error="${2:-true}"
if [ -z "$part" ]; then
return
fi
if [ "$show_error" = "false" ] || [ "$show_error" = "0" ]; then
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "begin $part; end" 2>/dev/null
else
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "begin $part; rescue Exception => e; YAML.LOG_ERROR('Set Custom Overwrite Script Failed,【%s】' % [e.message]); end" 2>/dev/null
fi
}
case "$op" in
ruby_arr_add_file|ruby_map_edit) arity=5;;
ruby_arr_edit) arity=6;;
ruby_arr_head_add_file|ruby_arr_insert|ruby_arr_insert_arr|ruby_arr_insert_hash|ruby_cover|ruby_merge) arity=4;;
ruby_delete|ruby_edit|ruby_merge_hash) arity=3;;
ruby_uniq) arity=2;;
*) return;;
esac
openclash_custom_overwrite() {
local pid=$$
while [ "$pid" != "1" ]; do
pname=$(tr -d '\0' < /proc/"$pid"/cmdline 2>/dev/null)
case "$pname" in
*openclash_custom_overwrite.sh*)
OVERWRITE_PARENT="custom"
return 0
;;
*yaml_overwrite.sh*|*/tmp/yaml_overwrite.sh*)
OVERWRITE_PARENT="yaml_overwrite"
return 0
;;
esac
pid=$(awk '/^PPid:/ {print $2}' /proc/"$pid"/status 2>/dev/null)
printf '%s' "$op" >> /tmp/yaml_openclash_custom_calls
for arg in "$@"; do
printf '\000%s' "$arg" >> /tmp/yaml_openclash_custom_calls
count=$((count + 1))
done
return 1
}
ruby_read()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 then puts Value$2 end"
if [ -n "$(echo "$2" |grep '.to_yaml' 2>/dev/null)" ]; then
run_ruby_part "$RUBY_YAML_PARSE" "false" |sed '1d' 2>/dev/null
else
run_ruby_part "$RUBY_YAML_PARSE" "false"
fi
}
ruby_read_hash()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
RUBY_YAML_PARSE="Value = $1; if Value$2 then puts Value$2 end"
run_ruby_part "$RUBY_YAML_PARSE" "false"
}
ruby_read_hash_arr()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 then Value$2.each do |i| if i$3 then puts i$3 end; end; end"
run_ruby_part "$RUBY_YAML_PARSE" "false"
while [ "$count" -lt "$arity" ]; do
printf '\000' >> /tmp/yaml_openclash_custom_calls
count=$((count + 1))
done
printf '\000' >> /tmp/yaml_openclash_custom_calls
}
ruby_edit()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; Value$2=$3"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); Value$2=$3; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_edit "$1" "$2" "$3"
}
ruby_cover()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if File::exist?('$3') then Value_1 = YAML.load_file('$3'); if not '$4'.empty? then Value$2=Value_1['$4']; else Value$2=Value_1 end else if not '$4'.empty? then Value.delete('$4'); end; end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if File::exist?('$3') then Value_1 = YAML.load_file('$3'); if not '$4'.empty? then Value$2=Value_1['$4']; else Value$2=Value_1 end else if not '$4'.empty? then Value.delete('$4'); end; end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_cover "$1" "$2" "$3" "$4"
}
ruby_merge()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; Value_1 = YAML.load_file('$3'); if not Value$2 then Value$2 = {}; end; if Value_1$4 && Value_1$4.is_a?(Hash) then Value$2.merge!(Value_1$4) end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if File::exist?('$3') then Value_1 = YAML.load_file('$3'); if not Value$2 then Value$2 = {}; end; if Value_1$4 && Value_1$4.is_a?(Hash) then Value$2.merge!(Value_1$4) end end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_merge "$1" "$2" "$3" "$4"
}
ruby_uniq()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if Value$2 then Value$2=Value$2.uniq; end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 then Value$2=Value$2.uniq; end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_uniq "$1" "$2"
}
ruby_merge_hash()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if not Value$2 then Value$2 = {}; end; Value$2.merge!({$3})"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if not Value$2 then Value$2 = {}; end; Value$2.merge!({$3}); YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_merge_hash "$1" "$2" "$3"
}
ruby_arr_add_file()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ] || [ -z "$5" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if File::exist?('$4') then Value_1 = YAML.load_file('$4'); if not Value$2 or Value$2.nil? then Value$2 = []; end; if Value_1$5 && Value_1$5.is_a?(Array) then idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; Value_1$5.reverse.each{|x| Value$2.insert(idx,x); idx += 1}; Value$2=Value$2.uniq end end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if File::exist?('$4') then Value_1 = YAML.load_file('$4'); if not Value$2 or Value$2.nil? then Value$2 = []; end; if Value_1$5 && Value_1$5.is_a?(Array) then idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; Value_1$5.reverse.each{|x| Value$2.insert(idx,x); idx += 1}; Value$2=Value$2.uniq end end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_arr_add_file "$1" "$2" "$3" "$4" "$5"
}
ruby_arr_head_add_file()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if File::exist?('$3') then Value_1 = YAML.load_file('$3'); if not Value$2 or Value$2.nil? then Value$2 = []; end; if Value_1$4 && Value_1$4.is_a?(Array) then Value$2=(Value_1$4+Value$2).uniq else Value$2=Value$2.uniq end end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if File::exist?('$3') then Value_1 = YAML.load_file('$3'); if not Value$2 or Value$2.nil? then Value$2 = []; end; if Value_1$4 && Value_1$4.is_a?(Array) then Value$2=(Value_1$4+Value$2).uniq else Value$2=Value$2.uniq end end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_arr_head_add_file "$1" "$2" "$3" "$4"
}
ruby_arr_insert()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if not Value$2 or Value$2.nil? then Value$2 = []; end; idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; Value$2=Value$2.insert(idx,'$4').uniq"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if not Value$2 or Value$2.nil? then Value$2 = []; end; idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; Value$2=Value$2.insert(idx,'$4').uniq; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_arr_insert "$1" "$2" "$3" "$4"
}
ruby_arr_insert_hash()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if not Value$2 or Value$2.nil? then Value$2 = []; end; idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; Value$2=Value$2.insert(idx,$4).uniq"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if not Value$2 or Value$2.nil? then Value$2 = []; end; idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; Value$2=Value$2.insert(idx,$4).uniq; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_arr_insert_hash "$1" "$2" "$3" "$4"
}
ruby_arr_insert_arr()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if not Value$2 or Value$2.nil? then Value$2 = []; end; if $4.is_a?(Array) then idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; $4.reverse.each{|x| Value$2=Value$2.insert(idx,x); idx += 1}; Value$2=Value$2.uniq end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if not Value$2 or Value$2.nil? then Value$2 = []; end; if $4.is_a?(Array) then idx = [$3.to_i, 0].max; idx = [idx, Value$2.length].min; $4.reverse.each{|x| Value$2=Value$2.insert(idx,x); idx += 1}; Value$2=Value$2.uniq end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_arr_insert_arr "$1" "$2" "$3" "$4"
}
ruby_delete()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$3" ]; then
return
fi
if openclash_custom_overwrite; then
if [ -z "$2" ]; then
RUBY_YAML_PARSE="yaml_file_path='$1'; Value.delete('$3')"
else
RUBY_YAML_PARSE="yaml_file_path='$1'; if Value$2 then if Value$2.is_a?(Hash) then Value$2.delete('$3') elsif Value$2.is_a?(Array) then Value$2.delete('$3') end end"
fi
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
if [ -z "$2" ]; then
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); Value.delete('$3'); YAML.dump(Value, '$1')"
else
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 then if Value$2.is_a?(Hash) then Value$2.delete('$3') elsif Value$2.is_a?(Array) then Value$2.delete('$3') end end; YAML.dump(Value, '$1')"
fi
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_delete "$1" "$2" "$3"
}
ruby_map_edit()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if Value$2 && Value$2.is_a?(Hash) then if Value$2['$3'] && Value$2['$3'].is_a?(Hash) then Value$2['$3']$4 = '$5' end end"
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 && Value$2.is_a?(Hash) then if Value$2['$3'] && Value$2['$3'].is_a?(Hash) then Value$2['$3']$4 = '$5' end end; YAML.dump(Value, '$1')"
run_ruby_part "$RUBY_YAML_PARSE"
ruby_record ruby_map_edit "$1" "$2" "$3" "$4" "$5"
}
ruby_arr_edit()
{
local RUBY_YAML_PARSE
if [ -z "$1" ] || [ -z "$2" ]; then
return
fi
if openclash_custom_overwrite; then
RUBY_YAML_PARSE="yaml_file_path='$1'; if Value$2 && Value$2.is_a?(Array) then"
if [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] && [ -n "$6" ]; then
RUBY_YAML_PARSE="$RUBY_YAML_PARSE Value$2.each{|x| if x.is_a?(Hash) && x$3 == '$4' then x$5='$6' end} end"
elif [ -z "$3" ] && [ -n "$4" ] && [ -z "$5" ] && [ -n "$6" ]; then
RUBY_YAML_PARSE="$RUBY_YAML_PARSE Value$2.map!{|x| if x == '$4' then '$6' else x end}; Value$2.uniq! end"
ruby_record ruby_arr_edit "$1" "$2" "$3" "$4" "$5" "$6"
}
ruby_read()
{
[ -z "$1" ] || [ -z "$2" ] && return
if [ -n "$(echo "$2" |grep '.to_yaml' 2>/dev/null)" ]; then
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e 'YAML.overwrite_read(ARGV[0], ARGV[1])' "$1" "$2" 2>/dev/null |sed '1d'
else
return
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e 'YAML.overwrite_read(ARGV[0], ARGV[1])' "$1" "$2" 2>/dev/null
fi
write_ruby_part "$RUBY_YAML_PARSE"
return
fi
if [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] && [ -n "$6" ]; then
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 && Value$2.is_a?(Array) then Value$2.map!{|x| if x.is_a?(Hash) && x$3 == '$4' then x$5='$6' end; x}; Value$2.uniq! end; YAML.dump(Value, '$1')"
elif [ -z "$3" ] && [ -n "$4" ] && [ -z "$5" ] && [ -n "$6" ]; then
RUBY_YAML_PARSE="Value = YAML.load_file('$1'); if Value$2 && Value$2.is_a?(Array) then Value$2.map!{|x| if x == '$4' then '$6' else x end}; Value$2.uniq! end; YAML.dump(Value, '$1')"
else
return
fi
run_ruby_part "$RUBY_YAML_PARSE"
}
}
ruby_read_hash()
{
[ -z "$1" ] || [ -z "$2" ] && return
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e 'YAML.overwrite_read_hash(ARGV[0], ARGV[1])' "$1" "$2" 2>/dev/null
}
ruby_read_hash_arr()
{
[ -z "$1" ] || [ -z "$2" ] && return
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e 'YAML.overwrite_read_hash_arr(ARGV[0], ARGV[1], ARGV[2])' "$1" "$2" "$3" 2>/dev/null
}
File diff suppressed because it is too large. Load diff
@@ -1,43 +1,141 @@
#!/bin/sh
. /lib/functions.sh
# The stock loader evaluates the whole export in the shell, seconds once the dynamic sections
# hold hundreds of nodes, and they are never read through config_get; the stages that need
# them call config_load_full.
uci_load() {
local PACKAGE="$1"
local DATA
local RET
local VAR
_C=0
if [ -z "$CONFIG_APPEND" ]; then
for VAR in $CONFIG_LIST_STATE; do
export ${NO_EXPORT:+-n} CONFIG_${VAR}=
export ${NO_EXPORT:+-n} CONFIG_${VAR}_LENGTH=
done
export ${NO_EXPORT:+-n} CONFIG_LIST_STATE=
export ${NO_EXPORT:+-n} CONFIG_SECTIONS=
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=0
export ${NO_EXPORT:+-n} CONFIG_SECTION=
fi
DATA="$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} ${LOAD_STATE:+-P /var/state} -S -n export "$PACKAGE" 2>/dev/null)"
RET="$?"
if [ "$RET" = 0 ] && [ -n "$DATA" ]; then
DATA="$(printf '%s\n' "$DATA" | awk -v keep=1 '/^config /{ keep = ($2 != "proxies" && $2 != "proxy_groups" && $2 != "proxy_providers") } keep')"
fi
[ "$RET" != 0 -o -z "$DATA" ] || eval "$DATA"
unset DATA
${CONFIG_SECTION:+config_cb}
return "$RET"
}
# Unfiltered loader, for the pipeline stages that build the YAML from the dynamic sections
uci_load_full() {
local PACKAGE="$1"
local DATA
local RET
local VAR
_C=0
if [ -z "$CONFIG_APPEND" ]; then
for VAR in $CONFIG_LIST_STATE; do
export ${NO_EXPORT:+-n} CONFIG_${VAR}=
export ${NO_EXPORT:+-n} CONFIG_${VAR}_LENGTH=
done
export ${NO_EXPORT:+-n} CONFIG_LIST_STATE=
export ${NO_EXPORT:+-n} CONFIG_SECTIONS=
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=0
export ${NO_EXPORT:+-n} CONFIG_SECTION=
fi
DATA="$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} ${LOAD_STATE:+-P /var/state} -S -n export "$PACKAGE" 2>/dev/null)"
RET="$?"
[ "$RET" != 0 -o -z "$DATA" ] || eval "$DATA"
unset DATA
${CONFIG_SECTION:+config_cb}
return "$RET"
}
config_load_full() {
[ -n "$IPKG_INSTROOT" ] && return 0
uci_load_full "$@"
}
uci_get_config() {
local key="$1"
uci -q get openclash.@overwrite[0]."$key" || uci -q get openclash.config."$key"
}
uci_get_age_public_keys() {
local name="$1"
# Prints one "<tag>\t<name>\t<value>" line per age key, tag P = public / S = secret, from a
# single "uci show" dump; a tag or a name restricts the output.
uci_age_keys_dump() {
[ -f /etc/config/openclash ] || return 0
grep -q config_age_secret /etc/config/openclash 2>/dev/null || return 0
uci -q show openclash 2>/dev/null | awk -v only_tag="$1" -v only_name="$2" '
BEGIN {
sq = sprintf("%c", 39)
escaped = sq sprintf("%c", 92) sq sq
tab = sprintf("%c", 9)
}
{
eq = index($0, "=")
if (eq == 0) next
key = substr($0, 1, eq - 1)
val = substr($0, eq + 1)
if (length(val) >= 2 && substr(val, 1, 1) == sq && substr(val, length(val), 1) == sq) {
val = substr(val, 2, length(val) - 2)
while ((p = index(val, escaped)) > 0) val = substr(val, 1, p - 1) sq substr(val, p + 4)
}
if (index(key, "openclash.") != 1) next
rest = substr(key, 11)
dot = 0
for (i = 1; i <= length(rest); i++) if (substr(rest, i, 1) == ".") dot = i
if (dot == 0) { type[rest] = val; next }
id = substr(rest, 1, dot - 1)
opt = substr(rest, dot + 1)
if (opt == "name") names[id] = val
else if (opt == "public") pubs[id] = (pubs[id] == "" ? val : pubs[id] "\n" val)
else if (opt == "secret") secs[id] = (secs[id] == "" ? val : secs[id] "\n" val)
}
END {
for (id in names) {
if (type[id] != "config_age_secret" || names[id] == "") continue
if (only_name != "" && names[id] != only_name) continue
if (only_tag != "S" && pubs[id] != "") {
c = split(pubs[id], v, "\n")
for (i = 1; i <= c; i++) print "P" tab names[id] tab v[i]
}
if (only_tag != "P" && secs[id] != "") {
c = split(secs[id], v, "\n")
for (i = 1; i <= c; i++) print "S" tab names[id] tab v[i]
}
}
}
'
}
uci_age_key_lookup() {
local field="$1" name="$2" tag
[ -n "$name" ] || return 0
_print_pub() {
local section="$1"
config_get cfg_name "$section" name
if [ "$cfg_name" = "$name" ]; then
config_get pub "$section" public
[ -n "$pub" ] && printf '%s\n' "$pub"
fi
}
case "$field" in
public) tag="P" ;;
secret) tag="S" ;;
*) return 0 ;;
esac
config_load openclash
config_foreach _print_pub config_age_secret
uci_age_keys_dump "$tag" "$name" | cut -f 3-
}
uci_get_age_secret_keys() {
local name="$1"
[ -n "$name" ] || return 0
_print_sec() {
local section="$1"
config_get cfg_name "$section" name
if [ "$cfg_name" = "$name" ]; then
config_get sec "$section" secret
[ -n "$sec" ] && printf '%s\n' "$sec"
fi
}
config_load openclash
config_foreach _print_sec config_age_secret
uci_age_key_lookup "secret" "$1"
}
uci_set_age_keys_by_name() {
@@ -11,23 +11,34 @@ custom_fakeip_filter=$(uci_get_config "custom_fakeip_filter" || echo 0)
custom_name_policy=$(uci_get_config "custom_name_policy" || echo 0)
custom_proxy_server_policy=$(uci_get_config "custom_proxy_server_policy" || echo 0)
custom_host=$(uci_get_config "custom_host" || echo 0)
system_hostname=$(uci -q get system.@system[0].hostname 2>/dev/null)
dnsmasq_domain=$(uci -q get dhcp.@dnsmasq[0].domain 2>/dev/null)
enable_custom_dns=$(uci_get_config "enable_custom_dns" || echo 0)
append_wan_dns=$(uci_get_config "append_wan_dns" || echo 0)
custom_fallback_filter=$(uci_get_config "custom_fallback_filter" || echo 0)
china_ip_route=$(uci_get_config "china_ip_route" || echo 0)
china_ip6_route=$(uci_get_config "china_ip6_route" || echo 0)
china_ip_route_domain_source=$(uci_get_config "china_ip_route_domain_source" || echo "mrs")
enable_redirect_dns=$(uci_get_config "enable_redirect_dns" || echo 1)
fake_ip_filter_mode=${34}
fake_ip_filter_mode="${33}"
[ -z "$fake_ip_filter_mode" ] && fake_ip_filter_mode="blacklist"
default_dashboard=$(uci_get_config "default_dashboard" || echo "metacubexd")
yacd_type=$(uci_get_config "yacd_type" || echo "Official")
dashboard_type=$(uci_get_config "dashboard_type" || echo "Official")
dashboard_forward_port=$(uci_get_config "dashboard_forward_port" || echo 0)
dashboard_forward_ssl=$(uci_get_config "dashboard_forward_ssl" || echo 0)
dashboard_custom_url=$(uci_get_config "dashboard_custom_url" || echo 0)
[ "$china_ip_route" -ne 0 ] && [ "$china_ip_route" -ne 1 ] && [ "$china_ip_route" -ne 2 ] && china_ip_route=0
[ "$china_ip6_route" -ne 0 ] && [ "$china_ip6_route" -ne 1 ] && [ "$china_ip6_route" -ne 2 ] && china_ip6_route=0
case "$china_ip_route_domain_source" in
mrs|geosite) ;;
*) china_ip_route_domain_source="mrs" ;;
esac
en_mode_tun=${11:-0}
if [ -z "${12}" ]; then
stack_type=${30:-"system"}
stack_type=${30:-"mips"}
else
stack_type=${12}
fi
@@ -35,25 +46,18 @@ fi
if [ "$1" = "fake-ip" ] && [ "$enable_redirect_dns" != "2" ]; then
TMP_FILTER_FILE="/tmp/yaml_openclash_fake_filter_include"
> "$TMP_FILTER_FILE"
process_pass_list() {
[ ! -f "$1" ] && return
awk -v mode="$fake_ip_filter_mode" '
awk '
!/^$/ && !/^#/ {
# 跳过IPv4和IPv6地址
if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(\/[0-9]+)?$/ || $0 ~ /:/) {
next
}
if (mode == "blacklist") {
if ($0 ~ /^\+?\./ || $0 ~ /^\*\./) {
print $0
} else {
print "+."$0
}
} else if (mode == "rule") {
domain = $0
sub(/^[\+\*\.]+/, "", domain)
print "DOMAIN-SUFFIX," domain ",real-ip"
sub(/^[\+\*\.]+/, "", $0)
if ($0 != "") {
print $0
}
}
' "$1" >> "$TMP_FILTER_FILE" 2>/dev/null
@@ -165,8 +169,7 @@ sys_dns_append()
PROXY_GROUPS=$(ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
begin
Value = YAML.load_file('$CONFIG_FILE')
File.open('/tmp/yaml_change_marshal', 'wb') { |f| Marshal.dump(Value, f) }
Value = YAML.load_file_cached('$CONFIG_FILE', '/tmp/yaml_change_marshal')
if Value.key?('proxy-groups') && Value['proxy-groups'].is_a?(Array)
Value['proxy-groups'].each { |x| puts x['name'] if x.key?('name') }
end
@@ -310,6 +313,10 @@ config_load "openclash"
config_foreach yml_auth_get "authentication"
yml_dns_custom "$enable_custom_dns" "$5" "$append_wan_dns" "${16}"
OPENCLASH_CORS_DOMAIN="${36}" \
OPENCLASH_CORS_PORT="$dashboard_forward_port" \
OPENCLASH_CORS_SSL="$dashboard_forward_ssl" \
OPENCLASH_CORS_CUSTOM_URL="$dashboard_custom_url" \
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
def safe_load_yaml(file_path)
@@ -326,18 +333,66 @@ def merge_list_from_file(dns_hash, key, file_path)
(dns_hash[key] ||= []).unshift(*lines).uniq!
end
def http_origin(raw_url)
raw = raw_url.to_s.strip
return nil if raw.empty?
scheme = raw[/\A(https?):\/\//i, 1]
return nil unless scheme
scheme = scheme.downcase
rest = raw.sub(/\Ahttps?:\/\//i, '')
return nil if rest.include?('@')
authority = rest.split(/[\/?#]/, 2).first.to_s
return nil if authority.empty?
host, port = if (m = authority.match(/\A\[([^\]]+)\](?::(\d+))?\z/))
[m[1], m[2]]
elsif (m = authority.match(/\A([^:]+)(?::(\d+))?\z/))
[m[1], m[2]]
else
[nil, nil]
end
return nil if host.nil? || host.empty?
host = '[' + host + ']' if host.include?(':')
default_port = scheme == 'https' ? 443 : 80
port = port ? port.to_i : default_port
return nil unless port.between?(1, 65_535)
scheme + '://' + host + (port == default_port ? '' : ':' + port.to_s)
rescue
nil
end
def controller_origin(domain, configured_port, ssl)
domain = domain.to_s.strip
return nil if domain.empty? || domain == '0'
return nil if domain.include?('@')
domain = domain.sub(/\Ahttps?:\/\//i, '')
port_text = configured_port.to_s.strip
authority = domain.split(/[\/?#]/, 2).first.to_s
embedded_port = authority[/\A\[[^\]]+\]:(\d+)\z/, 1] || authority[/\A[^:]+:(\d+)\z/, 1]
port_text = embedded_port if (port_text.empty? || port_text == '0') && embedded_port
scheme = ssl.to_s == '1' ? 'https' : 'http'
default_port = scheme == 'https' ? 443 : 80
port_text = default_port.to_s if port_text.empty? || port_text == '0'
return nil unless (port_text =~ /\A\d+\z/) && port_text.to_i.between?(1, 65_535)
host = if authority =~ /\A\[[^\]]+\]/
authority[/\A\[([^\]]+)\]/ , 1]
else
authority.split(':', 2).first
end
return nil if host.nil? || host.empty?
host = '[' + host + ']' if host.include?(':')
port = port_text.to_i
scheme + '://' + host + (port == default_port ? '' : ':' + port.to_s)
rescue
nil
end
begin
config_file = '$5'
if File.exist?('/tmp/yaml_change_marshal')
begin
Value = Marshal.load(File.open('/tmp/yaml_change_marshal', 'rb') { |f| f.read })
rescue
Value = YAML.load_file(config_file)
end
else
Value = YAML.load_file(config_file)
end
Value = YAML.load_file_cached(config_file, '/tmp/yaml_change_marshal')
rescue Exception => e
YAML.LOG_ERROR('Load File Failed,【%s】' % [e.message])
exit
@@ -373,10 +428,10 @@ begin
ipv6_mode = '${29}'
unified_delay = '${31}' == '1'
respect_rules = '${32}' == '1'
fake_ip_filter_mode = '${33}'
fake_ip_filter_mode = '$fake_ip_filter_mode'
routing_mark_setting = '${34}'
quic_gso = '${35}' == '1'
cors_origin = '${36}'
cors_origin = http_origin(ENV['OPENCLASH_CORS_CUSTOM_URL']) || controller_origin(ENV['OPENCLASH_CORS_DOMAIN'], ENV['OPENCLASH_CORS_PORT'], ENV['OPENCLASH_CORS_SSL'])
geo_custom_url = '${37}'
geoip_custom_url = '${38}'
geosite_custom_url = '${39}'
@@ -398,6 +453,7 @@ begin
custom_fakeip_filter = '$custom_fakeip_filter' == '1'
china_ip_route = '$china_ip_route' != '0'
china_ip6_route = '$china_ip6_route' != '0'
china_ip_route_domain_source = '$china_ip_route_domain_source'
custom_name_policy = '$custom_name_policy' == '1'
custom_proxy_server_policy = '$custom_proxy_server_policy' == '1'
custom_host = '$custom_host' == '1'
@@ -406,7 +462,7 @@ begin
Value['dns'] ||= {}
threads = []
threads << Thread.new do
threads << YAML::Inline.new do
begin
Value['redir-port'] = redir_port.to_i
Value['tproxy-port'] = tproxy_port.to_i
@@ -453,7 +509,7 @@ begin
Value['find-process-mode'] = find_process_mode if find_process_mode != '0'
(Value['experimental'] ||= {})['quic-go-disable-gso'] = true if quic_gso
if cors_origin != '0'
if cors_origin
(Value['external-controller-cors'] ||= {})['allow-origins'] = [cors_origin]
Value['external-controller-cors']['allow-private-network'] = true
end
@@ -501,8 +557,8 @@ begin
if enable_sniffer
sniffer_config = {
'enable' => true, 'override-destination' => true,
'sniff' => {'QUIC' => {'ports' => [443]}, 'TLS' => {'ports' => [443, 8443]}, 'HTTP' => {'ports' => [80, '8080-8880'], 'override-destination' => true}},
'enable' => true, 'override-destination' => false,
'sniff' => {'QUIC' => {'ports' => [443]}, 'TLS' => {'ports' => [443, 8443]}, 'HTTP' => {'ports' => [80, '8080-8880'], 'override-destination' => false}},
'force-domain' => ['+.netflix.com', '+.nflxvideo.net', '+.amazonaws.com', '+.media.dssott.com'],
'skip-domain' => ['Mijia Cloud', 'dlg.io.mi.com', '+.oray.com', '+.sunlogin.net', '+.push.apple.com']
}
@@ -521,7 +577,8 @@ begin
'enable' => true, 'stack' => stack_type, 'device' => 'utun',
'dns-hijack' => ['127.0.0.1:53'], 'endpoint-independent-nat' => true,
'auto-route' => false, 'auto-detect-interface' => false,
'auto-redirect' => false, 'strict-route' => false, 'disable-icmp-forwarding' => false
'auto-redirect' => false, 'strict-route' => false, 'disable-icmp-forwarding' => false,
'gso' => true
}
Value['tun'].delete('iproute2-table-index')
else
@@ -551,7 +608,7 @@ begin
end
end
threads << Thread.new do
threads << YAML::Inline.new do
begin
if enable_custom_dns || append_wan_dns
if (namedns_config = safe_load_yaml('/tmp/yaml_config.namedns.yaml')) && namedns_config['nameserver']
@@ -606,7 +663,7 @@ begin
end
# proxy-server-nameserver
threads << Thread.new do
threads << YAML::Inline.new do
begin
if enable_custom_dns
if (proxydns = safe_load_yaml('/tmp/yaml_config.proxynamedns.yaml')) && proxydns['proxy-server-nameserver']
@@ -619,7 +676,7 @@ begin
end
# direct-nameserver
threads << Thread.new do
threads << YAML::Inline.new do
begin
if enable_custom_dns
if (directdns = safe_load_yaml('/tmp/yaml_config.directnamedns.yaml')) && directdns['direct-nameserver']
@@ -632,7 +689,7 @@ begin
end
# nameserver-policy
threads << Thread.new do
threads << YAML::Inline.new do
begin
if custom_name_policy
if (policy = safe_load_yaml('/etc/openclash/custom/openclash_custom_domain_dns_policy.list'))
@@ -645,7 +702,7 @@ begin
end
# proxy-server-nameserver-policy
threads << Thread.new do
threads << YAML::Inline.new do
begin
if custom_proxy_server_policy
if (policy = safe_load_yaml('/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list'))
@@ -658,19 +715,41 @@ begin
end
# Fake-IP Filter
threads << Thread.new do
threads << YAML::Inline.new do
begin
if custom_fakeip_filter
Value['dns']['fake-ip-filter-mode'] = fake_ip_filter_mode
if fake_ip_mode == 'fake-ip'
merge_list_from_file(Value['dns'], 'fake-ip-filter', '/etc/openclash/custom/openclash_custom_fake_filter.list')
merge_list_from_file(Value['dns'], 'fake-ip-filter', '/tmp/yaml_openclash_fake_filter_include')
end
end
# Set chnroute_pass domains
if fake_ip_mode == 'fake-ip' && enable_redirect_dns != '2' && File.exist?('/tmp/yaml_openclash_fake_filter_include')
pass_domains = File.readlines('/tmp/yaml_openclash_fake_filter_include').map { |l| l.gsub(/#.*$/, '').strip }.reject(&:empty?).uniq
if pass_domains.any?
filter_mode = Value.dig('dns', 'fake-ip-filter-mode')
if filter_mode == 'whitelist'
filters = Value.dig('dns', 'fake-ip-filter')
if filters.is_a?(Array)
deleted_filters = filters.select { |f| pass_domains.any? { |d| f.to_s.include?(d) } }
if deleted_filters.any?
Value['dns']['fake-ip-filter'] -= deleted_filters
deleted_filters.each do |f|
YAML.LOG_TIP('Because Need Ensure Bypassing IP Option Work, Deleted The Fake-IP-Filter Rule【%s】...' % [f])
end
end
end
else
filter_rules = filter_mode == 'rule' ? pass_domains.map { |d| 'DOMAIN-SUFFIX,' + d + ',real-ip' } : pass_domains.map { |d| '+.' + d }
(Value['dns']['fake-ip-filter'] ||= []).unshift(*filter_rules).uniq!
YAML.LOG_TIP('Because Need Ensure Bypassing IP Option Work, Added The Fake-IP-Filter Rule【%s】...' % [filter_rules.join(', ')])
end
end
end
if fake_ip_mode == 'fake-ip' && (china_ip_route || china_ip6_route)
filter_mode = Value.dig('dns', 'fake-ip-filter-mode')
if filter_mode == 'blacklist' || filter_mode.nil?
filter_rule = 'rule-set:oc-cn-domain'
filter_rule = china_ip_route_domain_source == 'geosite' ? 'geosite:cn' : 'rule-set:oc-cn-domain'
(Value['dns']['fake-ip-filter'] ||= []) << filter_rule
end
if filter_mode == 'whitelist'
@@ -684,23 +763,41 @@ begin
end
end
if filter_mode == 'rule'
filter_rule = 'RULE-SET,oc-cn-domain,real-ip'
filter_rule = china_ip_route_domain_source == 'geosite' ? 'GEOSITE,cn,real-ip' : 'RULE-SET,oc-cn-domain,real-ip'
(Value['dns']['fake-ip-filter'] ||= []).unshift(filter_rule)
end
Value['dns']['fake-ip-filter'].uniq!
filters = Value.dig('dns', 'fake-ip-filter')
filters.uniq! if filters.is_a?(Array)
if filter_mode != 'whitelist'
rule_set_hash = {'rule-providers+'=>{'oc-cn-domain'=>{'type'=>'http', 'interval'=>43200, 'behavior'=>'domain', 'format'=>'mrs', 'url'=>'https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/cn.mrs', 'path'=> './rule_provider/oc-cn-domain.mrs'}}}
Value = YAML.overwrite(Value, rule_set_hash)
if china_ip_route_domain_source == 'mrs'
rule_set_hash = {'rule-providers+'=>{'oc-cn-domain'=>{'type'=>'http', 'interval'=>43200, 'behavior'=>'domain', 'format'=>'mrs', 'url'=>'https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/cn.mrs', 'path'=> './rule_provider/oc-cn-domain.mrs'}}}
Value = YAML.overwrite(Value, rule_set_hash)
end
YAML.LOG_TIP('Because Need Ensure Bypassing IP Option Work, Added The Fake-IP-Filter Rule【%s】...' % [filter_rule])
end
end
if fake_ip_mode == 'fake-ip'
cn_domain_provider_ready = (china_ip_route || china_ip6_route) && filter_mode != 'whitelist' && china_ip_route_domain_source == 'mrs'
unless cn_domain_provider_ready || Value.dig('rule-providers', 'oc-cn-domain')
dangling_filters = Value.dig('dns', 'fake-ip-filter')
if dangling_filters.is_a?(Array)
dangling_filters = dangling_filters.select { |f| f.to_s.strip.match?(/\A(rule-set:oc-cn-domain|RULE-SET,oc-cn-domain,real-ip)\z/i) }
if dangling_filters.any?
Value['dns']['fake-ip-filter'] -= dangling_filters
dangling_filters.each do |f|
YAML.LOG_WARN('Rule-Provider【oc-cn-domain】Is Not Registered, Deleted The Fake-IP-Filter Rule【%s】to Avoid Core Start Failed...' % [f])
end
end
end
end
end
rescue Exception => e
YAML.LOG_ERROR('Set Fake-IP-Filter Failed,【%s】' % [e.message])
end
end
# Custom Hosts
threads << Thread.new do
threads << YAML::Inline.new do
begin
if custom_host
if (hosts_content = safe_load_yaml('/etc/openclash/custom/openclash_custom_hosts.list')) && !hosts_content.empty?
@@ -719,7 +816,7 @@ begin
end
# Authentication
threads << Thread.new do
threads << YAML::Inline.new do
begin
if (auth_config = safe_load_yaml('/tmp/yaml_openclash_auth'))
Value['authentication'] = auth_config
@@ -734,11 +831,30 @@ begin
begin
threads.clear
# Built-in hosts for common OpenWrt router hostnames, resolve to router LAN IP
begin
Value['hosts'] ||= {}
builtin_hosts = {
'openwrt.lan' => 'lan',
'immortalwrt.lan' => 'lan',
'lede.lan' => 'lan',
'router.lan' => 'lan'
}
system_hostname = '$system_hostname'
dnsmasq_domain = '$dnsmasq_domain'
builtin_hosts[system_hostname.to_s + '.lan'] = 'lan' unless system_hostname.to_s.empty?
builtin_hosts[system_hostname.to_s + '.' + dnsmasq_domain.to_s] = 'lan' unless system_hostname.to_s.empty? || dnsmasq_domain.to_s.empty?
builtin_hosts.each { |k, v| Value['hosts'][k] ||= v }
Value['dns']['use-hosts'] = true
rescue Exception => e
YAML.LOG_ERROR('Set Built-in Hosts Failed,【%s】' % [e.message])
end
# DNS Loop Check
if enable_redirect_dns == '1'
dns_options = ['nameserver', 'fallback', 'default-nameserver', 'proxy-server-nameserver', 'nameserver-policy', 'direct-nameserver', 'proxy-server-nameserver-policy']
dns_options.each do |option|
threads << Thread.new(option) do |opt|
threads << YAML::Inline.new(option) do |opt|
begin
next unless Value['dns'].key?(opt) && !Value['dns'][opt].nil?
if opt != 'nameserver-policy' && opt != 'proxy-server-nameserver-policy'
@@ -821,9 +937,10 @@ rescue Exception => e
ensure
begin
YAML.dump(Value, config_file)
YAML.cache_write(config_file, Value, '/tmp/yaml_change_marshal')
rescue Exception => e
YAML.LOG_ERROR('Write file failed:【%s】' % [e.message])
File.delete('/tmp/yaml_change_marshal') rescue nil
end
File.delete('/tmp/yaml_change_marshal') rescue nil
end
" 2>/dev/null >> $LOG_FILE
" 2>/dev/null >> $LOG_FILE
@@ -49,205 +49,15 @@ fi
LOG_OUT "Start Getting【$CONFIG_NAME】Groups Setting..."
LOG_OUT "Deleting Old Configuration..."
del_options()
{
local section="$1"
local config
config_get "config" "$section" "config" ""
if [ "$config" = "$CONFIG_NAME" ]; then
uci -q delete openclash."$section"
fi
}
config_load "openclash"
config_foreach del_options "groups"
config_foreach del_options "servers"
config_foreach del_options "proxy-provider"
uci -q commit openclash
# The proxy-groups block is imported by uci.rb, its field table is the one the set
# direction reads back.
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
begin
Value = YAML.load_file('$CONFIG_FILE');
rescue Exception => e
YAML.LOG_ERROR('Load File Failed,【' + e.message + '】');
require 'uci';
failed = UCI.import_groups('$CONFIG_FILE', '$CONFIG_NAME');
if not failed.empty? then
YAML.LOG_ERROR('Write Uci Config Failed,【' + failed.map{ |t, e| t.to_s + ' => ' + e.to_s }.join('; ') + '】');
end;
threads_g = [];
threadsp = [];
threads_uci = [];
uci_commands = [];
uci_name_tmp = [];
max_threads = 30
queue = SizedQueue.new(max_threads)
if not Value.key?('proxy-groups') or Value['proxy-groups'].nil? then
proxy-groups = [];
end;
CONFIG_GROUP = (['DIRECT', 'REJECT', 'GLOBAL', 'REJECT-DROP', 'PASS', 'COMPATIBLE'] + (Value['proxy-groups']&.map { |x| x['name'] } || [])).uniq;
Value['proxy-groups'].each_with_index do |x, index|
uci_name_tmp << %x{uci -q add openclash groups 2>&1}.chomp
queue.push(nil)
threadsp << Thread.new {
begin
next unless x['name'] && x['type'];
uci_set='set openclash.' + uci_name_tmp[index] + '.'
uci_add='add_list openclash.' + uci_name_tmp[index] + '.'
YAML.LOG('Start Getting【${CONFIG_NAME} - ' + x['type'].to_s + ' - ' + x['name'].to_s + '】Group Setting...');
threads_g << Thread.new {
#name
if x.key?('name') then
uci_commands << uci_set + 'name=\"' + x['name'].to_s + '\"'
end;
};
threads_g << Thread.new {
#type
if x.key?('type') then
uci_commands << uci_set + 'type=\"' + x['type'].to_s + '\"'
end;
};
threads_g << Thread.new {
#enabled
uci_commands << uci_set + 'enabled=\"1\"'
#config
uci_commands << uci_set + 'config=\"' + '${CONFIG_NAME}' + '\"'
#old_name
uci_commands << uci_set + 'old_name=\"' + x['name'] + '\"'
};
threads_g << Thread.new {
#strategy
if x.key?('strategy') and x['type'] == 'load-balance' then
uci_commands << uci_set + 'strategy=\"' + x['strategy'].to_s + '\"'
end;
};
threads_g << Thread.new {
#strategy-smart
if x.key?('strategy') and x['type'] == 'smart' then
uci_commands << uci_set + 'strategy_smart=\"' + x['strategy'].to_s + '\"'
end;
};
threads_g << Thread.new {
#uselightgbm
if x.key?('uselightgbm') and x['type'] == 'smart' then
uci_commands << uci_set + 'uselightgbm=\"' + x['uselightgbm'].to_s + '\"'
end;
};
threads_g << Thread.new {
#collectdata
if x.key?('collectdata') and x['type'] == 'smart' then
uci_commands << uci_set + 'collectdata=\"' + x['collectdata'].to_s + '\"'
end;
};
threads_g << Thread.new {
#policy_priority
if x.key?('policy-priority') and x['type'] == 'smart' then
uci_commands << uci_set + 'policy_priority=\"' + x['policy-priority'].to_s + '\"'
end;
};
threads_g << Thread.new {
#disable-udp
if x.key?('disable-udp') then
uci_commands << uci_set + 'disable_udp=\"' + x['disable-udp'].to_s + '\"'
end;
};
threads_g << Thread.new {
if x['type'] == 'url-test' or x['type'] == 'fallback' or x['type'] == 'load-balance' or x['type'] == 'smart' then
#test_url
if x.key?('url') then
uci_commands << uci_set + 'test_url=\"' + x['url'].to_s + '\"'
end;
#test_interval
if x.key?('interval') then
uci_commands << uci_set + 'test_interval=\"' + x['interval'].to_s + '\"'
end;
#test_tolerance
if x['type'] == 'url-test' then
if x.key?('tolerance') then
uci_commands << uci_set + 'tolerance=\"' + x['tolerance'].to_s + '\"'
end;
end;
end;
};
threads_g << Thread.new {
#Policy Filter
if x.key?('filter') then
uci_commands << uci_set + 'policy_filter=\"' + x['filter'].to_s + '\"'
end
};
threads_g << Thread.new {
#interface-name
if x.key?('interface-name') then
uci_commands << uci_set + 'interface_name=\"' + x['interface-name'].to_s + '\"'
end
};
threads_g << Thread.new {
#routing-mark
if x.key?('routing-mark') then
uci_commands << uci_set + 'routing_mark=\"' + x['routing-mark'].to_s + '\"'
end
};
threads_g << Thread.new {
#other_group
if x.key?('proxies') then
x['proxies'].each{
|y|
if CONFIG_GROUP.include?(y) then
uci_commands << uci_add + 'other_group=\"^' + y.to_s + '$\"'
end
}
end
};
threads_g << Thread.new {
#icon
if x.key?('icon') then
uci_commands << uci_set + 'icon=\"' + x['icon'].to_s + '\"'
end
};
threads_g.each(&:join);
rescue Exception => e
YAML.LOG_ERROR('Resolve Groups Failed,【${CONFIG_NAME} - ' + x['type'] + ' - ' + x['name'] + ': ' + e.message + '】');
ensure
queue.pop
end;
};
end;
threadsp.each(&:join);
batch_size = 30;
(0...uci_commands.length).step(batch_size) do |i|
threads_uci << Thread.new{
IO.popen('uci -q batch', 'w') do |pipe|
uci_commands[i, batch_size].each { |cmd| pipe.puts(cmd) }
end
};
end;
threads_uci.each(&:join);
uci_name_tmp.each do |x|
if x =~ /uci -q delete/ then
system(x);
end;
end;
system('uci -q commit openclash');
system('rm -rf /tmp/yaml_other_group.yaml 2>/dev/null');
" 2>/dev/null >> $LOG_FILE
" >> $LOG_FILE 2>&1
/usr/share/openclash/yml_proxys_get.sh "$CONFIG_FILE" >/dev/null 2>&1
del_lock
del_lock
@@ -42,289 +42,21 @@ if [ -z "$CONFIG_NAME" ]; then
CONFIG_NAME="config.yaml"
fi
set_groups()
{
if [ -z "$1" ]; then
return
fi
# Keep the ruby heap small, the group matching loop reads the whole package into memory
RUBY_GC_HEAP_GROWTH_FACTOR=1.1
RUBY_GC_HEAP_GROWTH_MAX_SLOTS=200000
RUBY_GC_MALLOC_LIMIT_MAX=67108864
export RUBY_GC_HEAP_GROWTH_FACTOR RUBY_GC_HEAP_GROWTH_MAX_SLOTS RUBY_GC_MALLOC_LIMIT_MAX
if [ "$add_for_this" -eq 1 ]; then
return
fi
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
require 'uci';
begin
UCI.write_groups('$GROUP_FILE', '${CONFIG_NAME}');
rescue ::Exception => e
YAML.LOG_ERROR('Resolve Groups Failed,【${CONFIG_NAME} - ' + e.message + '】');
end;
" 2>/dev/null >> $LOG_FILE
if [ "$1" = "all" ] || [[ "$3" =~ ${1} ]] || [ -n "$(echo ${3} |grep -Eo ${1})" ]; then
set_group=1
add_for_this=1
echo " - \"${2}\"" >>$GROUP_FILE
fi
}
#加入节点
yml_servers_add()
{
local section="$1"
local enabled config name
add_for_this=0
config_get_bool "enabled" "$section" "enabled" "1"
config_get "config" "$section" "config" ""
config_get "name" "$section" "name" ""
if [ -n "$config" ] && [ "$config" != "$CONFIG_NAME" ] && [ "$config" != "all" ]; then
return
fi
if [ "$enabled" = "0" ]; then
return
else
if [ -z "$4" ]; then
config_list_foreach "$section" "groups" set_groups "$name" "$2"
fi
fi
}
add_other_group()
{
local section="$1"
local name enabled config
config_get_bool "enabled" "$section" "enabled" "1"
config_get "config" "$section" "config" ""
config_get "name" "$section" "name" ""
if [ "$enabled" = "0" ]; then
return
fi
if [ -n "$config" ] && [ "$config" != "$CONFIG_NAME" ] && [ "$config" != "all" ]; then
return
fi
if [ -z "$name" ]; then
return
fi
if [ "$3" = "$name" ]; then
return
fi
if [ "$2" = "all" ] || [[ "$name" =~ ${2} ]] || [ -n "$(echo ${name} |grep -Eo ${2})" ]; then
set_group=1
echo " - ${name}" >>$GROUP_FILE
fi
}
#加入其它策略组
set_other_groups()
{
if [ -z "$1" ]; then
return
fi
if [[ "$1" =~ "DIRECT" ]] || [ -n "$(echo ${1} |grep 'DIRECT')" ]; then
set_group=1
echo " - DIRECT" >>$GROUP_FILE
return
fi
if [[ "$1" =~ "REJECT" ]] || [ -n "$(echo ${1} |grep 'REJECT')" ]; then
set_group=1
echo " - REJECT" >>$GROUP_FILE
return
fi
if [[ "$1" =~ "REJECT-DROP" ]] || [ -n "$(echo ${1} |grep 'REJECT-DROP')" ]; then
set_group=1
echo " - REJECT-DROP" >>$GROUP_FILE
return
fi
if [[ "$1" =~ "PASS" ]] || [ -n "$(echo ${1} |grep 'PASS')" ]; then
set_group=1
echo " - PASS" >>$GROUP_FILE
return
fi
if [[ "$1" =~ "GLOBAL" ]] || [ -n "$(echo ${1} |grep 'GLOBAL')" ]; then
set_group=1
echo " - GLOBAL" >>$GROUP_FILE
return
fi
config_foreach add_other_group "groups" "$1" "$2" #比对策略组
}
#加入代理集
set_proxy_provider()
{
local section="$1"
local enabled config name
add_for_this=0
config_get_bool "enabled" "$section" "enabled" "1"
config_get "config" "$section" "config" ""
config_get "name" "$section" "name" ""
if [ -n "$config" ] && [ "$config" != "$CONFIG_NAME" ] && [ "$config" != "all" ]; then
return
fi
if [ "$enabled" = "0" ]; then
return
else
if [ -z "$3" ]; then
config_list_foreach "$section" "groups" set_provider_groups "$name" "$2"
fi
fi
}
set_provider_groups()
{
if [ -z "$1" ]; then
return
fi
if [ "$add_for_this" -eq 1 ]; then
return
fi
if [[ "$3" =~ ${1} ]] || [ -n "$(echo ${3} |grep -Eo ${1})" ] || [ "$1" = "all" ]; then
set_proxy_provider=1
add_for_this=1
echo " - ${2}" >>$GROUP_FILE
fi
}
#创建策略组
yml_groups_set()
{
local section="$1"
local enabled config type name disable_udp strategy old_name test_url test_interval tolerance policy_filter uselightgbm collectdata policy_priority other_parameters icon
config_get_bool "enabled" "$section" "enabled" "1"
config_get "config" "$section" "config" ""
config_get "type" "$section" "type" ""
config_get "name" "$section" "name" ""
config_get "disable_udp" "$section" "disable_udp" ""
config_get "strategy" "$section" "strategy" ""
config_get "old_name" "$section" "old_name" ""
config_get "test_url" "$section" "test_url" ""
config_get "test_interval" "$section" "test_interval" ""
config_get "tolerance" "$section" "tolerance" ""
config_get "policy_filter" "$section" "policy_filter" ""
config_get "uselightgbm" "$section" "uselightgbm" ""
config_get "collectdata" "$section" "collectdata" ""
config_get "policy_priority" "$section" "policy_priority" ""
config_get "other_parameters" "$section" "other_parameters" ""
config_get "icon" "$section" "icon" ""
if [ "$enabled" = "0" ]; then
return
fi
if [ -n "$config" ] && [ "$config" != "$CONFIG_NAME" ] && [ "$config" != "all" ]; then
return
fi
if [ -z "$type" ]; then
return
fi
if [ -z "$name" ]; then
return
fi
LOG_OUT "Start Writing【$CONFIG_NAME - $type - $name】Group To Config File..."
echo " - name: $name" >>$GROUP_FILE
echo " type: $type" >>$GROUP_FILE
echo " proxies: $name" >>$GROUP_FILE
set_group=0
set_proxy_provider=0
config_list_foreach "$section" "other_group" set_other_groups "$name" #加入其他策略组
config_foreach yml_servers_add "servers" "$name" "$type" #加入服务器节点
echo " use: $name" >>$GROUP_FILE
config_foreach set_proxy_provider "proxy-provider" "$name" #加入代理集
#Prevent the un support symbol name
name=$(echo "$name" |sed 's/\//\\\//g' 2>/dev/null)
if [ "$set_group" -eq 1 ]; then
sed -i "/^ \{0,\}proxies: ${name}/c\ proxies:" $GROUP_FILE
else
sed -i "/proxies: ${name}/d" $GROUP_FILE 2>/dev/null
fi
if [ "$set_proxy_provider" -eq 1 ]; then
sed -i "/^ \{0,\}use: ${name}/c\ use:" $GROUP_FILE
else
sed -i "/use: ${name}/d" $GROUP_FILE 2>/dev/null
fi
if [ "$set_group" -eq 0 ] && [ "$set_proxy_provider" -eq 0 ]; then
echo " proxies:" >>$GROUP_FILE
echo " - DIRECT" >>$GROUP_FILE
fi
if [ "$type" = "load-balance" ]; then
[ -n "$strategy" ] && {
echo " strategy: $strategy" >>$GROUP_FILE
}
fi
[ -n "$disable_udp" ] && {
echo " disable-udp: $disable_udp" >>$GROUP_FILE
}
[ -n "$test_url" ] && {
echo " url: $test_url" >>$GROUP_FILE
}
[ -n "$test_interval" ] && {
echo " interval: \"$test_interval\"" >>$GROUP_FILE
}
[ -n "$tolerance" ] && {
echo " tolerance: \"$tolerance\"" >>$GROUP_FILE
}
[ -n "$policy_filter" ] && {
echo " filter: \"$policy_filter\"" >>$GROUP_FILE
}
[ -n "$interface_name" ] && {
echo " interface-name: \"$interface_name\"" >>$GROUP_FILE
}
[ -n "$routing_mark" ] && {
echo " routing-mark: \"$routing_mark\"" >>$GROUP_FILE
}
if [ "$type" = "smart" ]; then
[ -n "$uselightgbm" ] && {
echo " uselightgbm: $uselightgbm" >>$GROUP_FILE
}
[ -n "$collectdata" ] && {
echo " collectdata: $collectdata" >>$GROUP_FILE
}
[ -n "$policy_priority" ] && {
echo " policy-priority: \"$policy_priority\"" >>$GROUP_FILE
}
fi
#icon
if [ -n "$icon" ]; then
echo " icon: $icon" >> "$GROUP_FILE"
fi
#other_parameters
if [ -n "$other_parameters" ]; then
echo -e "$other_parameters" >> "$GROUP_FILE"
fi
}
echo "proxy-groups:" >$GROUP_FILE
config_load "openclash"
config_foreach yml_groups_set "groups"
sed -i "s/#delete_//g" "$CONFIG_FILE" 2>/dev/null
/usr/share/openclash/yml_proxys_set.sh "$CONFIG_FILE" >/dev/null 2>&1
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -14,7 +14,7 @@ yml_other_set()
{
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
begin
Value = YAML.load_file('$2');
Value = YAML.load_file_cached('$2', '/tmp/yaml_change_marshal');
rescue Exception => e
YAML.LOG_ERROR('Load File Failed,【' + e.message + '】');
end;
@@ -24,11 +24,11 @@ yml_other_set()
# GEOIP replace
geoip_pattern = /^GEOIP,([A-Za-z]{2}),([^,]+)(,.*)?/;
match_pattern = /(^MATCH.*|^FINAL.*)/;
thread_pool << Thread.new{
thread_pool << YAML::Inline.new{
#BT/P2P DIRECT Rules
begin
if $3 == 1 then
if system('strings /etc/openclash/GeoSite.dat /etc/openclash/GeoSite.dat |grep -i category-public-tracker >/dev/null 2>&1') then
if system('grep -qai category-public-tracker /etc/openclash/GeoSite.dat >/dev/null 2>&1') then
bt_rules = ['GEOSITE,category-public-tracker,DIRECT'];
else
bt_rules = [
@@ -126,6 +126,7 @@ yml_other_set()
rescue Exception => e
CONFIG_GROUP = ['DIRECT', 'REJECT', 'GLOBAL', 'REJECT-DROP', 'PASS', 'COMPATIBLE'];
end;
CONFIG_GROUP_HASH = CONFIG_GROUP.map{|x| [x, true]}.to_h;
#Custom Rules
begin
@@ -147,9 +148,10 @@ yml_other_set()
anchors_orig[:geo_rule] = nil
end;
custom_data_cache = {};
custom_files.each{|file_info|
if File::exist?(file_info[:file]) then
custom_data = YAML.load_file(file_info[:file]);
custom_data = custom_data_cache.fetch(file_info[:file]){ custom_data_cache[file_info[:file]] = YAML.load_file(file_info[:file]); };
next if custom_data == false;
rules_array = case custom_data.class.to_s
@@ -201,7 +203,7 @@ yml_other_set()
valid_rules = transformed_rules.select{|x|
RULE_GROUP = ((x.split(',')[-1] =~ rule_suffix_regex) ? x.split(',')[-2] : x.split(',')[-1]).strip;
if CONFIG_GROUP.include?(RULE_GROUP) then
if CONFIG_GROUP_HASH[RULE_GROUP] then
true;
else
YAML.LOG_WARN('Skiped The Custom Rule Because Group & Proxy Not Found:【' + x + '】');
@@ -211,7 +213,7 @@ yml_other_set()
if Value.has_key?('rules') and not Value['rules'].to_a.empty? then
if file_info[:position] == 'top' then
valid_rules.reverse.each{|x| Value['rules'].insert(0,x)};
Value['rules'] = valid_rules + Value['rules'];
else
ruby_add_index = nil;
if anchors_orig[:dst80_rule]
@@ -225,8 +227,11 @@ yml_other_set()
end;
ruby_add_index ||= -1;
insert_rules = ruby_add_index == -1 ? valid_rules : valid_rules.reverse;
insert_rules.each{|x| Value['rules'].insert(ruby_add_index,x)};
if ruby_add_index == -1 then
Value['rules'] = Value['rules'] + valid_rules;
else
Value['rules'] = Value['rules'][0...ruby_add_index] + valid_rules + Value['rules'][ruby_add_index..-1];
end;
end;
Value['rules'] = Value['rules'].uniq;
else
@@ -244,7 +249,7 @@ yml_other_set()
['sub-rules'].each{|key|
custom_files.each{|file_info|
if File::exist?(file_info[:file]) then
custom_data = YAML.load_file(file_info[:file]);
custom_data = custom_data_cache.fetch(file_info[:file]){ custom_data_cache[file_info[:file]] = YAML.load_file(file_info[:file]); };
if custom_data != false and custom_data.class.to_s == 'Hash' then
if not custom_data[key].to_a.empty? and custom_data[key].class.to_s == 'Hash' then
if Value.has_key?(key) and not Value[key].to_a.empty? then
@@ -281,7 +286,7 @@ yml_other_set()
end;
};
thread_pool << Thread.new{
thread_pool << YAML::Inline.new{
threads = [];
#provider CDN
@@ -290,17 +295,17 @@ yml_other_set()
provider_configs.each do |provider_type, path_prefix|
if Value.key?(provider_type) && Value[provider_type].is_a?(Hash) then
Value[provider_type].each{|name, config|
threads << Thread.new {
threads << YAML::Inline.new {
# CDN
if '$github_address_mod' != '0' and config['url'] then
if config['url'] =~ /^https:\/\/raw.githubusercontent.com/ then
if '$github_address_mod' == 'https://cdn.jsdelivr.net/' or
'$github_address_mod' == 'https://fastly.jsdelivr.net/' or
if '$github_address_mod' == 'https://cdn.jsdelivr.net/' or
'$github_address_mod' == 'https://fastly.jsdelivr.net/' or
'$github_address_mod' == 'https://testingcf.jsdelivr.net/' then
url_parts = config['url'].split('/');
if url_parts.length >= 5 then
config['url'] = '$github_address_mod' + 'gh/' + url_parts[3] + '/' +
url_parts[4] + '@' + config['url'].split(url_parts[2] +
config['url'] = '$github_address_mod' + 'gh/' + url_parts[3] + '/' +
url_parts[4] + '@' + config['url'].split(url_parts[2] +
'/' + url_parts[3] + '/' + url_parts[4] + '/')[1];
end;
else
@@ -322,7 +327,7 @@ yml_other_set()
begin
if '$tolerance' != '0' and Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
threads << YAML::Inline.new {
if group['type'] == 'url-test' then
group['tolerance'] = ${tolerance};
end;
@@ -338,7 +343,7 @@ yml_other_set()
if '$urltest_interval_mod' != '0' then
if Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
threads << YAML::Inline.new {
if ['url-test', 'fallback', 'load-balance', 'smart'].include?(group['type']) then
group['interval'] = ${urltest_interval_mod};
end;
@@ -347,7 +352,7 @@ yml_other_set()
end;
if Value.key?('proxy-providers') then
Value['proxy-providers'].each{|name, provider|
threads << Thread.new {
threads << YAML::Inline.new {
if provider['health-check'] and provider['health-check']['enable'] then
provider['health-check']['interval'] = ${urltest_interval_mod};
end;
@@ -364,7 +369,7 @@ yml_other_set()
if '$urltest_address_mod' != '0' then
if Value.key?('proxy-providers') then
Value['proxy-providers'].each{|name, provider|
threads << Thread.new {
threads << YAML::Inline.new {
if provider['health-check'] and provider['health-check']['enable'] then
provider['health-check']['url'] = '$urltest_address_mod';
end;
@@ -373,7 +378,7 @@ yml_other_set()
end;
if Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
threads << YAML::Inline.new {
if ['url-test', 'fallback', 'load-balance', 'smart'].include?(group['type']) then
group['url'] = '$urltest_address_mod';
end;
@@ -389,7 +394,7 @@ yml_other_set()
begin
if ('${8}' == '1' or '${9}' == '1' or '${11}' != '0' or '${12}' != '0' or '${12}' == '1' or '${13}' == '1' or '${14}' != '0') and Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
threads << YAML::Inline.new {
if '${8}' == '1' and ['url-test', 'load-balance'].include?(group['type']) then
group['type'] = 'smart';
end;
@@ -431,6 +436,7 @@ yml_other_set()
rescue Exception => e
YAML.LOG_ERROR('Write file failed:【%s】' % [e.message])
end
File.delete('/tmp/yaml_change_marshal') rescue nil
end" 2>/dev/null >> $LOG_FILE
}
@@ -0,0 +1,186 @@
#!/bin/sh
# One-line JSON summary of a config file for the subscription result view.
# Rule lines are read without their trailing options (no-resolve/no-resolve6/src).
# Age encrypted files are decrypted by YAML.rb; a missing key degrades to need_key=1.
# The device ruby ships without the json gem, so the JSON is serialized by hand.
CONFIG_FILE="$1"
if [ -z "$CONFIG_FILE" ] || [ ! -s "$CONFIG_FILE" ]; then
echo '{"status":"error","message":"config file missing"}'
exit 0
fi
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 - "$CONFIG_FILE" 2>/dev/null <<'RUBY'
def jstr(str)
str.to_s.gsub("\\") { "\\\\" }.gsub("\"") { "\\\"" }.gsub("\n") { "\\n" }.gsub("\r") { "" }.gsub("\t") { "\\t" }
end
def jdump(obj)
case obj
when Hash
"{" + obj.map { |k, v| "\"" + jstr(k) + "\":" + jdump(v) }.join(",") + "}"
when Array
"[" + obj.map { |v| jdump(v) }.join(",") + "]"
when Integer, Float
obj.to_s
when TrueClass, FalseClass
obj ? "true" : "false"
when NilClass
"null"
else
"\"" + jstr(obj) + "\""
end
end
path = ARGV[0]
res = { "status" => "success" }
begin
res["mtime"] = File.mtime(path).strftime("%Y-%m-%d %H:%M:%S")
header = File.binread(path, 512).to_s
res["age"] = header.include?("BEGIN AGE ENCRYPTED FILE")
cfg = YAML.load_file(path)
cfg = {} unless cfg.is_a?(Hash)
proxies = cfg["proxies"].is_a?(Array) ? cfg["proxies"] : []
pprov = cfg["proxy-providers"].is_a?(Hash) ? cfg["proxy-providers"] : {}
groups = cfg["proxy-groups"].is_a?(Array) ? cfg["proxy-groups"] : []
rules = cfg["rules"].is_a?(Array) ? cfg["rules"] : []
rprov = cfg["rule-providers"].is_a?(Hash) ? cfg["rule-providers"] : {}
types = {}
proxies.each do |proxy|
t = (proxy.is_a?(Hash) ? proxy["type"] : nil).to_s
t = "unknown" if t.empty?
types[t] = types[t].to_i + 1
end
prov_items = []
prov_nodes = {}
prov_nodes_total = 0
pprov.each do |pname, pconf|
count = nil
ptypes = {}
if pconf.is_a?(Hash)
candidates = []
ppath = pconf["path"].to_s
candidates << ppath if ppath.start_with?("/")
candidates << "/etc/openclash/proxy_provider/#{pname}.yaml"
candidates.each do |cand|
next if cand.empty? || !File.exist?(cand)
begin
pf = YAML.load_file(cand)
rescue StandardError
next
end
next unless pf.is_a?(Hash) && pf["proxies"].is_a?(Array)
count = pf["proxies"].size
pf["proxies"].each do |px|
t = (px.is_a?(Hash) ? px["type"] : nil).to_s
t = "unknown" if t.empty?
ptypes[t] = ptypes[t].to_i + 1
end
break
end
end
prov_nodes[pname.to_s] = count if count
prov_nodes_total += count if count
prov_items << { "name" => pname.to_s, "nodes" => count, "types" => ptypes }
end
group_names = {}
groups.each do |group|
next unless group.is_a?(Hash)
group_names[group["name"].to_s] = true
end
ref_counts = {}
groups.each do |group|
next unless group.is_a?(Hash)
seen = {}
mlist = group["proxies"].is_a?(Array) ? group["proxies"] : []
mlist.each do |m|
m = m.to_s
next if m == group["name"].to_s || !group_names[m] || seen[m]
seen[m] = true
ref_counts[m] = ref_counts[m].to_i + 1
end
end
items = []
groups.each do |group|
next unless group.is_a?(Hash)
t = group["type"].to_s
next if items.size >= 80
grefs = ref_counts[group["name"].to_s].to_i
node_count = 0
members = []
member_names = group["proxies"].is_a?(Array) ? group["proxies"] : []
member_names.each do |m|
m = m.to_s
if m == "DIRECT"
members << { "k" => "direct", "v" => "DIRECT" }
elsif m.start_with?("REJECT") || m == "PASS"
members << { "k" => "reject", "v" => m }
elsif group_names[m]
members << { "k" => "ref", "v" => m }
else
node_count += 1
end
end
if group["use"].is_a?(Array)
group["use"].each do |u|
u = u.to_s
members << { "k" => "provider", "v" => u }
node_count += prov_nodes[u] if prov_nodes[u]
end
end
members << { "k" => "all", "v" => ".*" } if group["include-all-proxies"] == true
members << { "k" => "nodes", "v" => node_count } if node_count > 0
items << { "n" => group["name"].to_s, "t" => t, "refs" => grefs, "members" => members }
end
refs = 0
rtypes = {}
targets = {}
target_order = []
# options at the end of a rule line must not be mistaken for the target
rule_opts = { "no-resolve" => true, "no-resolve6" => true, "src" => true }
rules.each do |rule|
s = rule.to_s.strip
next if s.empty?
parts = s.split(",").map { |p| p.to_s.strip }
parts.pop while parts.size >= 3 && rule_opts[parts.last.downcase]
next if parts.size < 2
rtype = parts[0].upcase
rtypes[rtype] = rtypes[rtype].to_i + 1
refs += 1 if rtype == "RULE-SET"
target = parts.last.to_s
next if target.empty?
unless targets[target]
targets[target] = { "t" => target, "c" => 0, "sets" => [], "rules" => [] }
target_order << target
end
entry = targets[target]
entry["c"] += 1
label = parts[0...-1].join(",")
if rtype == "RULE-SET"
entry["sets"] << parts[1].to_s if parts.size >= 3 && entry["sets"].size < 48
else
entry["rules"] << label if !label.empty? && entry["rules"].size < 48
end
end
mode = "empty"
mode = "provider" if proxies.empty? && !pprov.empty?
mode = "proxies" unless proxies.empty?
res["mode"] = mode
res["nodes"] = { "total" => proxies.size, "types" => types }
res["providers"] = { "proxy" => pprov.size, "rule" => rprov.size, "nodes_total" => prov_nodes_total, "items" => prov_items }
res["groups"] = { "total" => groups.size, "items" => items }
res["rules"] = { "total" => rules.size, "refs" => refs, "types" => rtypes, "targets" => target_order.map { |name| targets[name] } }
rescue StandardError => err
msg = err.message.to_s
if msg.include?("Encrypted file") || msg.include?("age decrypt")
res["age"] = true
res["need_key"] = true
else
res["status"] = "error"
res["message"] = msg
end
end
puts jdump(res)
RUBY