vanken 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +26 -11
- data/data/coloring_rules.yml +67 -0
- data/docs/performance.md +10 -0
- data/docs/releases.md +2 -4
- data/docs/usage.md +51 -0
- data/lib/vanken/app/capture_controller.rb +66 -1
- data/lib/vanken/app/document.rb +102 -39
- data/lib/vanken/app/document_analysis.rb +75 -0
- data/lib/vanken/app/document_jobs.rb +52 -9
- data/lib/vanken/app/expert_info.rb +26 -0
- data/lib/vanken/app/io_graph.rb +47 -0
- data/lib/vanken/app/navigation.rb +77 -0
- data/lib/vanken/app/search_job.rb +62 -0
- data/lib/vanken/app/stats_job.rb +45 -0
- data/lib/vanken/capture/analyzer.rb +3 -1
- data/lib/vanken/capture/analyzer_process.rb +9 -6
- data/lib/vanken/capture/analyzer_worker.rb +2 -2
- data/lib/vanken/capture/filter_worker.rb +8 -2
- data/lib/vanken/capture/helper_main.rb +10 -3
- data/lib/vanken/capture/helper_options.rb +6 -2
- data/lib/vanken/capture/launcher.rb +1 -0
- data/lib/vanken/config/analysis_settings.rb +58 -0
- data/lib/vanken/config/columns.rb +138 -0
- data/lib/vanken/config/messages.rb +105 -0
- data/lib/vanken/config/preferences.rb +34 -8
- data/lib/vanken/config/profiles.rb +74 -0
- data/lib/vanken/config/sessions.rb +69 -0
- data/lib/vanken/config/yaml_file.rb +36 -0
- data/lib/vanken/core/coloring.rb +86 -0
- data/lib/vanken/core/frame_store.rb +2 -2
- data/lib/vanken/core/stores.rb +12 -0
- data/lib/vanken/gateway/detail_builder.rb +1 -1
- data/lib/vanken/gateway/display_capture_filter.rb +102 -0
- data/lib/vanken/gateway/dissector.rb +31 -6
- data/lib/vanken/gateway/exporter.rb +125 -0
- data/lib/vanken/gateway/file_writer.rb +26 -2
- data/lib/vanken/gateway/interfaces.rb +59 -0
- data/lib/vanken/gateway/resolver.rb +131 -0
- data/lib/vanken/gateway/statistics.rb +83 -0
- data/lib/vanken/gateway/stream.rb +159 -0
- data/lib/vanken/ui/actions.rb +54 -28
- data/lib/vanken/ui/analysis_dialogs.rb +371 -0
- data/lib/vanken/ui/application.rb +183 -9
- data/lib/vanken/ui/coloring_operations.rb +104 -0
- data/lib/vanken/ui/column_operations.rb +115 -0
- data/lib/vanken/ui/dialogs.rb +41 -26
- data/lib/vanken/ui/file_operations.rb +17 -12
- data/lib/vanken/ui/filter_operations.rb +43 -15
- data/lib/vanken/ui/main_view.rb +74 -29
- data/lib/vanken/ui/navigation_operations.rb +86 -0
- data/lib/vanken/ui/packet_source.rb +33 -1
- data/lib/vanken/ui/selection.rb +8 -2
- data/lib/vanken/ui/settings_operations.rb +233 -0
- data/lib/vanken/version.rb +1 -1
- data/sig/generated/vanken/app/capture_controller.rbs +16 -0
- data/sig/generated/vanken/app/document.rbs +23 -1
- data/sig/generated/vanken/app/document_analysis.rbs +21 -0
- data/sig/generated/vanken/app/document_jobs.rbs +6 -0
- data/sig/generated/vanken/app/expert_info.rbs +30 -0
- data/sig/generated/vanken/app/io_graph.rbs +45 -0
- data/sig/generated/vanken/app/navigation.rbs +31 -0
- data/sig/generated/vanken/app/search_job.rbs +21 -0
- data/sig/generated/vanken/app/stats_job.rbs +18 -0
- data/sig/generated/vanken/capture/helper_options.rbs +6 -0
- data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
- data/sig/generated/vanken/config/columns.rbs +37 -0
- data/sig/generated/vanken/config/messages.rbs +14 -0
- data/sig/generated/vanken/config/preferences.rbs +1 -1
- data/sig/generated/vanken/config/profiles.rbs +29 -0
- data/sig/generated/vanken/config/sessions.rbs +17 -0
- data/sig/generated/vanken/config/yaml_file.rbs +11 -0
- data/sig/generated/vanken/core/coloring.rbs +52 -0
- data/sig/generated/vanken/core/stores.rbs +4 -0
- data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
- data/sig/generated/vanken/gateway/dissector.rbs +15 -2
- data/sig/generated/vanken/gateway/exporter.rbs +27 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
- data/sig/generated/vanken/gateway/resolver.rbs +52 -0
- data/sig/generated/vanken/gateway/statistics.rbs +104 -0
- data/sig/generated/vanken/gateway/stream.rbs +75 -0
- data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
- data/sig/generated/vanken/ui/application.rbs +23 -1
- data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
- data/sig/generated/vanken/ui/column_operations.rbs +29 -0
- data/sig/generated/vanken/ui/dialogs.rbs +5 -3
- data/sig/generated/vanken/ui/file_operations.rbs +2 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
- data/sig/generated/vanken/ui/main_view.rbs +14 -2
- data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
- data/sig/generated/vanken/ui/packet_source.rbs +4 -0
- data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
- metadata +51 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
require "resolv"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
module Vanken
|
|
9
|
+
module Gateway
|
|
10
|
+
# Reverse DNS never runs on the requesting thread. Callbacks run on the
|
|
11
|
+
# worker; UI callers must post their publication to the UI executor.
|
|
12
|
+
class Resolver
|
|
13
|
+
NEGATIVE_TTL = 30.0
|
|
14
|
+
|
|
15
|
+
# @rbs @timeout: Float
|
|
16
|
+
# @rbs @limit: Integer
|
|
17
|
+
# @rbs @resolver: untyped
|
|
18
|
+
# @rbs @queue: SizedQueue[String?]
|
|
19
|
+
# @rbs @cache: Hash[String, [String?, Float?]]
|
|
20
|
+
# @rbs @pending: Hash[String, Array[^(String) -> void]]
|
|
21
|
+
# @rbs @mutex: Mutex
|
|
22
|
+
# @rbs @closed: bool
|
|
23
|
+
# @rbs @worker: Thread
|
|
24
|
+
|
|
25
|
+
# @rbs (?timeout: Integer | Float, ?limit: Integer, ?resolver: untyped) -> void
|
|
26
|
+
def initialize(timeout: 1.0, limit: 256, resolver: nil)
|
|
27
|
+
unless timeout.is_a?(Numeric) && timeout.to_f.finite? && timeout.positive?
|
|
28
|
+
raise ArgumentError, "timeout must be finite and positive"
|
|
29
|
+
end
|
|
30
|
+
raise ArgumentError, "limit must be a positive integer" unless limit.is_a?(Integer) && limit.positive?
|
|
31
|
+
@timeout, @limit = timeout.to_f, limit
|
|
32
|
+
@resolver = resolver || Resolv::DNS.new
|
|
33
|
+
@resolver.timeouts = [@timeout] if @resolver.respond_to?(:timeouts=)
|
|
34
|
+
@queue = SizedQueue.new(@limit) #: SizedQueue[String?]
|
|
35
|
+
@cache = {} #: Hash[String, [String?, Float?]]
|
|
36
|
+
@pending = {} #: Hash[String, Array[^(String) -> void]]
|
|
37
|
+
@mutex = Mutex.new
|
|
38
|
+
@closed = false
|
|
39
|
+
@worker = Thread.new { work }
|
|
40
|
+
@worker.report_on_exception = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @rbs (String address) ?{ (String) -> void } -> String
|
|
44
|
+
def request(address, &callback)
|
|
45
|
+
key = numeric_address(address)
|
|
46
|
+
return address unless key
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
return address if @closed
|
|
49
|
+
entry = @cache.delete(key)
|
|
50
|
+
if entry && (!entry[1] || entry[1] > now)
|
|
51
|
+
@cache[key] = entry
|
|
52
|
+
return entry[0] || address
|
|
53
|
+
end
|
|
54
|
+
if (waiting = @pending[key])
|
|
55
|
+
waiting << callback if callback
|
|
56
|
+
else
|
|
57
|
+
begin
|
|
58
|
+
@queue.push(key, true)
|
|
59
|
+
@pending[key] = callback ? [callback] : []
|
|
60
|
+
rescue ThreadError
|
|
61
|
+
# A future viewport request may retry after capacity is available.
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
address
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @rbs () -> void
|
|
69
|
+
def close
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
return if @closed
|
|
72
|
+
@closed = true
|
|
73
|
+
@pending.clear
|
|
74
|
+
@queue.clear
|
|
75
|
+
@queue << nil
|
|
76
|
+
end
|
|
77
|
+
@worker.join(@timeout + 0.1) unless Thread.current == @worker
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# @rbs (String address) -> String?
|
|
84
|
+
def numeric_address(address)
|
|
85
|
+
return nil if address.include?("/")
|
|
86
|
+
IPAddr.new(address).to_s
|
|
87
|
+
rescue IPAddr::Error
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @rbs () -> Float
|
|
92
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
93
|
+
|
|
94
|
+
# @rbs (String address) -> String?
|
|
95
|
+
def lookup(address)
|
|
96
|
+
name = Timeout.timeout(@timeout) { @resolver.getname(address) }
|
|
97
|
+
name.to_s.dup.freeze unless name.nil? || name.to_s.empty?
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @rbs () -> void
|
|
103
|
+
def work
|
|
104
|
+
while (address = @queue.pop)
|
|
105
|
+
name = lookup(address)
|
|
106
|
+
callbacks = @mutex.synchronize do
|
|
107
|
+
return if @closed
|
|
108
|
+
@cache[address] = [name, name ? nil : now + NEGATIVE_TTL]
|
|
109
|
+
@cache.shift while @cache.size > @limit
|
|
110
|
+
@pending.delete(address) || []
|
|
111
|
+
end
|
|
112
|
+
next unless name
|
|
113
|
+
callbacks.each do |callback|
|
|
114
|
+
break if @mutex.synchronize { @closed }
|
|
115
|
+
begin
|
|
116
|
+
callback.call(name)
|
|
117
|
+
rescue StandardError
|
|
118
|
+
# One consumer must not prevent other consumers or DNS requests.
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
ensure
|
|
123
|
+
begin
|
|
124
|
+
@resolver.close if @resolver.respond_to?(:close)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative "dissector"
|
|
5
|
+
|
|
6
|
+
module Vanken
|
|
7
|
+
module Gateway
|
|
8
|
+
HierarchyRow = Data.define(
|
|
9
|
+
:path, #: Array[Symbol]
|
|
10
|
+
:packets, #: Integer
|
|
11
|
+
:bytes #: Integer
|
|
12
|
+
)
|
|
13
|
+
ConversationRow = Data.define(
|
|
14
|
+
:addr_a, #: String
|
|
15
|
+
:port_a, #: Integer
|
|
16
|
+
:addr_b, #: String
|
|
17
|
+
:port_b, #: Integer
|
|
18
|
+
:packets_ab, #: Integer
|
|
19
|
+
:packets_ba, #: Integer
|
|
20
|
+
:bytes_ab, #: Integer
|
|
21
|
+
:bytes_ba, #: Integer
|
|
22
|
+
:start_ns, #: Integer
|
|
23
|
+
:end_ns, #: Integer
|
|
24
|
+
:duration_ns #: Integer
|
|
25
|
+
)
|
|
26
|
+
EndpointRow = Data.define(
|
|
27
|
+
:address, #: String
|
|
28
|
+
:port, #: Integer
|
|
29
|
+
:packets_tx, #: Integer
|
|
30
|
+
:packets_rx, #: Integer
|
|
31
|
+
:bytes_tx, #: Integer
|
|
32
|
+
:bytes_rx #: Integer
|
|
33
|
+
)
|
|
34
|
+
StatisticsTable = Data.define(
|
|
35
|
+
:kind, #: Symbol
|
|
36
|
+
:type, #: Symbol?
|
|
37
|
+
:rows, #: Array[HierarchyRow | ConversationRow | EndpointRow]
|
|
38
|
+
:evicted #: Integer
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# The upstream session and its private table schema stay inside the Gateway.
|
|
42
|
+
class Statistics
|
|
43
|
+
# @rbs (?specs: Array[String], ?registry: Redhound::Registry, **untyped options) -> void
|
|
44
|
+
def initialize(specs: ["phs"], registry: Redhound::Registry.default, **options)
|
|
45
|
+
@session = Redhound::Analysis::Session.new(stats: specs, registry: registry, **options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs (PacketView packet) -> void
|
|
49
|
+
def update(packet) = @session.update(packet.raw_packet)
|
|
50
|
+
|
|
51
|
+
# @rbs () -> Array[StatisticsTable]
|
|
52
|
+
def tables
|
|
53
|
+
@session.statistics.map do |table|
|
|
54
|
+
value = table.to_h
|
|
55
|
+
rows = value[:rows].map do |row|
|
|
56
|
+
fields = row.dup #: Hash[Symbol, untyped]
|
|
57
|
+
fields[:path] = fields[:path].dup.freeze if fields[:path]
|
|
58
|
+
fields.each_value { |item| item.freeze if item.is_a?(String) }
|
|
59
|
+
case value[:kind]
|
|
60
|
+
when :phs
|
|
61
|
+
HierarchyRow.new(path: fields.fetch(:path), packets: fields.fetch(:packets), bytes: fields.fetch(:bytes))
|
|
62
|
+
when :conv
|
|
63
|
+
ConversationRow.new(addr_a: fields.fetch(:addr_a), port_a: fields.fetch(:port_a),
|
|
64
|
+
addr_b: fields.fetch(:addr_b), port_b: fields.fetch(:port_b),
|
|
65
|
+
packets_ab: fields.fetch(:packets_ab), packets_ba: fields.fetch(:packets_ba),
|
|
66
|
+
bytes_ab: fields.fetch(:bytes_ab), bytes_ba: fields.fetch(:bytes_ba),
|
|
67
|
+
start_ns: fields.fetch(:start_ns), end_ns: fields.fetch(:end_ns), duration_ns: fields.fetch(:duration_ns))
|
|
68
|
+
when :endpoints
|
|
69
|
+
EndpointRow.new(address: fields.fetch(:address), port: fields.fetch(:port),
|
|
70
|
+
packets_tx: fields.fetch(:packets_tx), packets_rx: fields.fetch(:packets_rx),
|
|
71
|
+
bytes_tx: fields.fetch(:bytes_tx), bytes_rx: fields.fetch(:bytes_rx))
|
|
72
|
+
else raise Vanken::Error, "invalid upstream statistics table"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
StatisticsTable.new(kind: value[:kind], type: value[:type], rows: rows.freeze, evicted: table.evicted)
|
|
76
|
+
end.freeze
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs () -> void
|
|
80
|
+
def close = @session.finish(StringIO.new, StringIO.new)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "dissector"
|
|
6
|
+
|
|
7
|
+
module Vanken
|
|
8
|
+
module Gateway
|
|
9
|
+
StreamChunk = Data.define(
|
|
10
|
+
:direction, #: Integer
|
|
11
|
+
:bytes, #: String
|
|
12
|
+
:missing, #: Integer
|
|
13
|
+
:number, #: Integer
|
|
14
|
+
:sequence #: Integer
|
|
15
|
+
)
|
|
16
|
+
StreamText = Data.define(
|
|
17
|
+
:direction, #: Integer
|
|
18
|
+
:text #: String
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class Stream
|
|
22
|
+
DISPLAY_LIMIT = 16 << 20 #: Integer
|
|
23
|
+
SEQUENCE_MOD = 1 << 32 #: Integer
|
|
24
|
+
attr_reader :nodes #: Array[String]
|
|
25
|
+
attr_reader :chunks #: Array[StreamChunk]
|
|
26
|
+
attr_reader :stream_id #: Integer
|
|
27
|
+
|
|
28
|
+
# @rbs (untyped document, Integer stream_id, ?cancelled: (^() -> bool)?) -> void
|
|
29
|
+
def initialize(document, stream_id, cancelled: nil)
|
|
30
|
+
raise ArgumentError, "invalid stream number" unless stream_id.is_a?(Integer) && stream_id >= 0
|
|
31
|
+
@stream_id, @nodes, @chunks = stream_id, [], []
|
|
32
|
+
pieces = [[], []] #: Array[Array[StreamChunk]]
|
|
33
|
+
starts = [nil, nil] #: Array[Integer?]
|
|
34
|
+
frontiers = [0, 0]
|
|
35
|
+
frames = document.stream_frames(stream_id)
|
|
36
|
+
frames.each do |number|
|
|
37
|
+
raise Vanken::Error, "operation cancelled" if cancelled&.call || document.closing?
|
|
38
|
+
packet = document.packet(number)
|
|
39
|
+
tcp = packet.layers.find { |layer| layer.protocol == :tcp && !layer.embedded }
|
|
40
|
+
next unless tcp
|
|
41
|
+
columns = packet.columns
|
|
42
|
+
endpoints = [endpoint(columns[:source], columns[:src_port]), endpoint(columns[:destination], columns[:dst_port])]
|
|
43
|
+
@nodes = tcp[:flags] & 0x12 == 0x12 ? endpoints.reverse : endpoints if @nodes.empty?
|
|
44
|
+
direction = endpoints == @nodes ? 0 : 1
|
|
45
|
+
sequence = document.annotations[number][:seq_rel]
|
|
46
|
+
difference = (sequence - frontiers[direction]) % SEQUENCE_MOD
|
|
47
|
+
difference -= SEQUENCE_MOD if difference >= SEQUENCE_MOD / 2
|
|
48
|
+
sequence = frontiers[direction] + difference
|
|
49
|
+
sequence += 1 if tcp[:flags] & 2 != 0
|
|
50
|
+
starts[direction] ||= sequence if tcp[:flags] & 2 != 0
|
|
51
|
+
data = packet.bytes.byteslice(tcp.payload_offset, tcp.payload_end - tcp.payload_offset)
|
|
52
|
+
frontiers[direction] = [frontiers[direction], sequence + data.bytesize].max
|
|
53
|
+
next if data.empty?
|
|
54
|
+
insert(pieces[direction], StreamChunk.new(direction: direction, bytes: data, missing: 0, number: number, sequence: sequence))
|
|
55
|
+
end
|
|
56
|
+
directions = pieces.each_with_index.map do |segments, direction|
|
|
57
|
+
cursor = starts[direction]
|
|
58
|
+
segments.flat_map do |segment|
|
|
59
|
+
missing = cursor && segment.sequence > cursor ? segment.sequence - cursor : 0
|
|
60
|
+
cursor = segment.sequence + segment.bytes.bytesize
|
|
61
|
+
gap = StreamChunk.new(direction: direction, bytes: "".b, missing: missing, number: segment.number, sequence: segment.sequence - missing)
|
|
62
|
+
missing.positive? ? [gap, segment] : [segment]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
positions = [0, 0]
|
|
66
|
+
until positions.each_with_index.all? { |index, direction| index == directions[direction].size }
|
|
67
|
+
direction = [0, 1].reject { |value| positions[value] == directions[value].size }.min_by do |value|
|
|
68
|
+
directions[value][positions[value]].number
|
|
69
|
+
end
|
|
70
|
+
raise Vanken::Error, "invalid stream direction" unless direction
|
|
71
|
+
@chunks << directions[direction][positions[direction]]
|
|
72
|
+
positions[direction] += 1
|
|
73
|
+
end
|
|
74
|
+
@nodes.freeze
|
|
75
|
+
@chunks.freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @rbs (?direction: Integer?) -> Integer
|
|
79
|
+
def byte_size(direction: nil) = selected(direction).sum { |chunk| chunk.bytes.bytesize }
|
|
80
|
+
|
|
81
|
+
# @rbs (?format: Symbol, ?direction: Integer?, ?limit: Integer) -> Array[StreamText]
|
|
82
|
+
def preview(format: :ascii, direction: nil, limit: DISPLAY_LIMIT)
|
|
83
|
+
validate(format, direction)
|
|
84
|
+
raise ArgumentError, "invalid display limit" unless limit.is_a?(Integer) && limit >= 0
|
|
85
|
+
remaining = limit
|
|
86
|
+
result = [] #: Array[StreamText]
|
|
87
|
+
selected(direction).each do |chunk|
|
|
88
|
+
break if remaining.zero?
|
|
89
|
+
bytes = chunk.bytes.byteslice(0, remaining) || "".b
|
|
90
|
+
remaining -= bytes.bytesize
|
|
91
|
+
result << StreamText.new(direction: chunk.direction, text: render(chunk, bytes, format))
|
|
92
|
+
end
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs (String path, ?format: Symbol, ?direction: Integer?, ?cancelled: (^() -> bool)?) -> String
|
|
97
|
+
def save(path, format: :raw, direction: nil, cancelled: nil)
|
|
98
|
+
validate(format, direction)
|
|
99
|
+
file = Tempfile.create([".vanken-stream-", ".tmp"], File.dirname(File.expand_path(path)))
|
|
100
|
+
begin
|
|
101
|
+
file.binmode
|
|
102
|
+
selected(direction).each do |chunk|
|
|
103
|
+
raise Vanken::Error, "operation cancelled" if cancelled&.call
|
|
104
|
+
file.write(render(chunk, chunk.bytes, format))
|
|
105
|
+
end
|
|
106
|
+
file.flush
|
|
107
|
+
file.fsync
|
|
108
|
+
file.close
|
|
109
|
+
File.rename(file.path, File.expand_path(path))
|
|
110
|
+
ensure
|
|
111
|
+
file.close unless file.closed?
|
|
112
|
+
FileUtils.rm_f(file.path)
|
|
113
|
+
end
|
|
114
|
+
path
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# @rbs (Integer? direction) -> Array[StreamChunk]
|
|
120
|
+
def selected(direction) = @chunks.select { |chunk| direction.nil? || chunk.direction == direction }
|
|
121
|
+
def endpoint(address, port) = address.include?(":") ? "[#{address}]:#{port}" : "#{address}:#{port}"
|
|
122
|
+
def validate(format, direction)
|
|
123
|
+
raise ArgumentError, "invalid stream format" unless %i[ascii hex raw].include?(format)
|
|
124
|
+
raise ArgumentError, "invalid stream direction" unless [nil, 0, 1].include?(direction)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def render(chunk, bytes, format)
|
|
128
|
+
return "".b if format == :raw && chunk.missing.positive?
|
|
129
|
+
return "[#{chunk.missing} bytes missing]" if chunk.missing.positive?
|
|
130
|
+
return bytes if format == :raw
|
|
131
|
+
return bytes.gsub(/[^\x20-\x7e\r\n\t]/n) { |byte| Kernel.format("\\x%02x", byte.getbyte(0)) } if format == :ascii
|
|
132
|
+
bytes.bytes.each_slice(16).with_index.map do |row, index|
|
|
133
|
+
Kernel.format("%08x %s\n", chunk.sequence + (index * 16), row.map { |byte| Kernel.format("%02x", byte) }.join(" "))
|
|
134
|
+
end.join
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Keep capture arrival precedence while inserting only previously unseen ranges.
|
|
138
|
+
def insert(segments, chunk)
|
|
139
|
+
first, last = chunk.sequence, chunk.sequence + chunk.bytes.bytesize
|
|
140
|
+
index = segments.bsearch_index { |segment| segment.sequence + segment.bytes.bytesize > first } || segments.size
|
|
141
|
+
uncovered = [] #: Array[[Integer, Integer]]
|
|
142
|
+
cursor = first
|
|
143
|
+
while index < segments.size && segments[index].sequence < last
|
|
144
|
+
previous = segments[index]
|
|
145
|
+
uncovered << [cursor, [previous.sequence, last].min] if previous.sequence > cursor
|
|
146
|
+
cursor = [cursor, previous.sequence + previous.bytes.bytesize].max
|
|
147
|
+
index += 1
|
|
148
|
+
end
|
|
149
|
+
uncovered << [cursor, last] if cursor < last
|
|
150
|
+
uncovered.each do |start, finish|
|
|
151
|
+
segment = chunk.with(sequence: start, bytes: chunk.bytes.byteslice(start - first, finish - start))
|
|
152
|
+
position = segments.bsearch_index { |previous| previous.sequence > start } || segments.size
|
|
153
|
+
# ponytail: array insertion is O(n) for heavily reordered streams; use an interval tree if measured.
|
|
154
|
+
segments.insert(position, segment)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/vanken/ui/actions.rb
CHANGED
|
@@ -5,44 +5,70 @@ module Vanken
|
|
|
5
5
|
module Actions
|
|
6
6
|
def self.install(ui)
|
|
7
7
|
commands = {
|
|
8
|
-
open: ["開く", "cmd-o", -> { ui.open_dialog }], save: ["名前を付けて保存", "cmd-shift-s", -> { ui.save_dialog }],
|
|
9
|
-
close_file: ["ファイルを閉じる", "cmd-w", -> { ui.close_document }], reload: ["再読み込み", "cmd-r", -> { ui.reload_file }],
|
|
10
|
-
quit: ["終了", "cmd-q", -> { ui.quit }], preferences: ["設定", "cmd-,", -> { ui.preferences_dialog }],
|
|
11
|
-
capture: ["キャプチャ開始/停止", "cmd-e", -> { ui.capture_toggle }], restart: ["キャプチャ再開", nil, -> { ui.restart_capture }],
|
|
12
|
-
apply_filter: ["フィルタを適用", "enter", -> { ui.apply_filter }], clear_filter: ["フィルタをクリア", nil, -> { ui.clear_filter }],
|
|
13
|
-
filter_history: ["フィルタ履歴", nil, -> { ui.filter_history }], bookmark: ["フィルタを保存", nil, -> { ui.bookmark_filter }],
|
|
14
|
-
filter_selected: ["選択した項目でフィルタ", nil, -> { ui.filter_from_selection }],
|
|
15
|
-
prepare_selected: ["選択した項目のフィルタを準備", nil, -> { ui.filter_from_selection(prepare: true) }],
|
|
16
|
-
columns: ["表示する列", nil, -> { ui.columns_dialog }], autoscroll: ["末尾追従", nil, -> { ui.toggle_autoscroll }],
|
|
17
|
-
zoom_in: ["拡大", "cmd-+", -> { ui.zoom(1) }], zoom_out: ["縮小", "cmd--", -> { ui.zoom(-1) }], zoom_reset: ["標準サイズ", "cmd-0", -> { ui.zoom }],
|
|
18
|
-
previous_packet: ["前のパケット", "alt-up", -> { ui.navigate(-1) }], next_packet: ["次のパケット", "alt-down", -> { ui.navigate(1) }],
|
|
19
|
-
first_packet: ["最初のパケット", nil, -> { ui.first_packet }], last_packet: ["最後のパケット", nil, -> { ui.last_packet }],
|
|
20
|
-
go_to_packet: ["パケットへ移動", "cmd-g", -> { ui.go_to_packet }], copy_filter: ["項目をフィルタとしてコピー", nil, -> { ui.copy_text(ui.selected_node.filter) if ui.selected_node&.filter }]
|
|
8
|
+
open: [ui.t("開く"), "cmd-o", -> { ui.open_dialog }], save: [ui.t("名前を付けて保存"), "cmd-shift-s", -> { ui.save_dialog }],
|
|
9
|
+
close_file: [ui.t("ファイルを閉じる"), "cmd-w", -> { ui.close_document }], reload: [ui.t("再読み込み"), "cmd-r", -> { ui.reload_file }],
|
|
10
|
+
quit: [ui.t("終了"), "cmd-q", -> { ui.quit }], preferences: [ui.t("設定"), "cmd-,", -> { ui.preferences_dialog }],
|
|
11
|
+
capture: [ui.t("キャプチャ開始/停止"), "cmd-e", -> { ui.capture_toggle }], restart: [ui.t("キャプチャ再開"), nil, -> { ui.restart_capture }],
|
|
12
|
+
apply_filter: [ui.t("フィルタを適用"), "enter", -> { ui.apply_filter }], clear_filter: [ui.t("フィルタをクリア"), nil, -> { ui.clear_filter }],
|
|
13
|
+
filter_history: [ui.t("フィルタ履歴"), nil, -> { ui.filter_history }], bookmark: [ui.t("フィルタを保存"), nil, -> { ui.bookmark_filter }],
|
|
14
|
+
filter_selected: [ui.t("選択した項目でフィルタ"), nil, -> { ui.filter_from_selection }],
|
|
15
|
+
prepare_selected: [ui.t("選択した項目のフィルタを準備"), nil, -> { ui.filter_from_selection(prepare: true) }],
|
|
16
|
+
columns: [ui.t("表示する列"), nil, -> { ui.columns_dialog }], autoscroll: [ui.t("末尾追従"), nil, -> { ui.toggle_autoscroll }],
|
|
17
|
+
zoom_in: [ui.t("拡大"), "cmd-+", -> { ui.zoom(1) }], zoom_out: [ui.t("縮小"), "cmd--", -> { ui.zoom(-1) }], zoom_reset: [ui.t("標準サイズ"), "cmd-0", -> { ui.zoom }],
|
|
18
|
+
previous_packet: [ui.t("前のパケット"), "alt-up", -> { ui.navigate(-1) }], next_packet: [ui.t("次のパケット"), "alt-down", -> { ui.navigate(1) }],
|
|
19
|
+
first_packet: [ui.t("最初のパケット"), nil, -> { ui.first_packet }], last_packet: [ui.t("最後のパケット"), nil, -> { ui.last_packet }],
|
|
20
|
+
go_to_packet: [ui.t("パケットへ移動"), "cmd-g", -> { ui.go_to_packet }], copy_filter: [ui.t("項目をフィルタとしてコピー"), nil, -> { ui.copy_text(ui.selected_node.filter) if ui.selected_node&.filter }]
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
commands.merge!(
|
|
23
|
+
find_packet: [ui.t("パケット検索"), "cmd-f", -> { ui.find_packet }], find_next: [ui.t("次を検索"), "cmd-n", -> { ui.find_next }], find_prev: [ui.t("前を検索"), "cmd-b", -> { ui.find_prev }],
|
|
24
|
+
mark_packet: [ui.t("マーク/解除"), "cmd-m", -> { ui.mark_packet }], mark_all_displayed: [ui.t("表示中をすべてマーク"), "cmd-shift-m", -> { ui.mark_all_displayed }], unmark_all: [ui.t("すべてのマークを解除"), "cmd-alt-m", -> { ui.unmark_all }],
|
|
25
|
+
ignore_packet: [ui.t("無視/解除"), "cmd-d", -> { ui.ignore_packet }], time_reference: [ui.t("時刻基準/解除"), "cmd-t", -> { ui.time_reference }],
|
|
26
|
+
prev_in_conversation: [ui.t("同一会話の前"), "cmd-,", -> { ui.prev_in_conversation }], next_in_conversation: [ui.t("同一会話の次"), "cmd-.", -> { ui.next_in_conversation }],
|
|
27
|
+
coloring_rules: [ui.t("色付けルール"), nil, -> { ui.coloring_dialog }], toggle_coloring: [ui.t("色付け"), nil, -> { ui.toggle_coloring }],
|
|
28
|
+
follow_tcp_stream: [ui.t("TCP ストリームを追跡"), "cmd-alt-shift-t", -> { ui.follow_tcp_stream }], decode_as: [ui.t("別プロトコルとして解析"), "cmd-shift-u", -> { ui.decode_as }],
|
|
29
|
+
expert_info: [ui.t("エキスパート情報"), nil, -> { ui.expert_info }], reload_plugins: [ui.t("プラグイン再読み込み"), "cmd-shift-l", -> { ui.reload_plugins }], plugins: [ui.t("ディセクタプラグイン"), nil, -> { ui.plugins_dialog }],
|
|
30
|
+
file_properties: [ui.t("キャプチャファイルのプロパティ"), "cmd-alt-shift-c", -> { ui.file_properties }], protocol_hierarchy: [ui.t("プロトコル階層"), nil, -> { ui.protocol_hierarchy }],
|
|
31
|
+
conversations: [ui.t("会話"), nil, -> { ui.conversations }], endpoints: [ui.t("端点"), nil, -> { ui.endpoints }], io_graph: [ui.t("I/O グラフ"), nil, -> { ui.io_graph }],
|
|
32
|
+
export_packets: [ui.t("指定パケットのエクスポート"), nil, -> { ui.export_packets }], export_dissections: [ui.t("解析結果のエクスポート"), nil, -> { ui.export_dissections }],
|
|
33
|
+
command_palette: [ui.t("コマンドパレット"), "cmd-shift-k", -> { ui.command_palette }], profiles: [ui.t("プロファイル"), nil, -> { ui.profiles_dialog }])
|
|
34
|
+
commands[:preferences][1] = "cmd-shift-p"
|
|
35
|
+
commands[:restart][1] = "cmd-shift-r"
|
|
36
|
+
{"relative" => "相対時刻", "absolute" => "絶対時刻", "delta" => "前パケットとの差", "delta_displayed" => "前表示パケットとの差", "epoch" => "エポック時刻"}.each { |name, label| commands[:"time_format_#{name}"] = [ui.t("時刻: %{name}", name: ui.t(label)), nil, -> { ui.time_format(name) }] }
|
|
37
|
+
{"system" => "システム設定", "dark" => "ダーク", "light" => "ライト", "high_contrast" => "ハイコントラスト"}.each { |name, label| commands[:"theme_#{name}"] = [ui.t("テーマ: %{name}", name: ui.t(label)), nil, -> { ui.change_theme(name) }] }
|
|
38
|
+
%i[selected not and and_not or or_not].zip([ui.t("選択項目"), ui.t("選択項目を除外"), "AND", "AND NOT", "OR", "OR NOT"]).each do |mode, label|
|
|
39
|
+
commands[:"filter_#{mode}"] = [ui.t("適用: %{label}", label: label), nil, -> { ui.filter_from_selection(mode) }]
|
|
40
|
+
commands[:"prepare_#{mode}"] = [ui.t("準備: %{label}", label: label), nil, -> { ui.filter_from_selection(mode, prepare: true) }]
|
|
25
41
|
end
|
|
26
|
-
{hex: "16 進数", text: "テキスト", dump: "ダンプ", escaped: "エスケープした文字列"}.each do |format, label|
|
|
27
|
-
commands[:"copy_#{format}"] = ["バイトをコピー:
|
|
42
|
+
{hex: ui.t("16 進数"), text: ui.t("テキスト"), dump: ui.t("ダンプ"), escaped: ui.t("エスケープした文字列")}.each do |format, label|
|
|
43
|
+
commands[:"copy_#{format}"] = [ui.t("バイトをコピー: %{label}", label: label), nil, -> { ui.copy_bytes(format) }]
|
|
28
44
|
end
|
|
29
45
|
commands.each do |name, (title, keys, operation)|
|
|
30
|
-
|
|
46
|
+
needs_document = %i[find_packet find_next find_prev mark_packet mark_all_displayed unmark_all ignore_packet time_reference prev_in_conversation next_in_conversation follow_tcp_stream expert_info file_properties protocol_hierarchy conversations endpoints io_graph export_packets export_dissections]
|
|
47
|
+
enabled = ->(*) { !needs_document.include?(name) || !!ui.document }
|
|
48
|
+
checked = name == :autoscroll ? ->(*) { ui.autoscroll? } : name == :toggle_coloring ? ->(*) { ui.coloring_enabled? } : nil
|
|
49
|
+
ui.app.actions.register(name, title: title, enabled: enabled, checked: checked) { |_| operation.call }
|
|
31
50
|
next unless keys
|
|
32
|
-
key = RUBY_PLATFORM.include?("darwin") ? keys : keys.sub("cmd", "ctrl")
|
|
51
|
+
key = ui.native? && RUBY_PLATFORM.include?("darwin") ? keys : keys.sub("cmd", "ctrl")
|
|
33
52
|
ui.window.dispatcher.keymap.bind(key, name, context: name == :apply_filter ? "in_display_filter && !in_completion" : "")
|
|
34
53
|
end
|
|
35
54
|
ui.app.menu_bar = Zaniah::Menu.build do
|
|
36
|
-
submenu("ファイル") { %i[open save close_file reload quit].each { |action| item(action) } }
|
|
37
|
-
submenu("編集") { %i[copy_filter copy_hex copy_text copy_dump copy_escaped preferences].each { |action| item(action) } }
|
|
38
|
-
submenu("表示")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
submenu(ui.t("ファイル")) { %i[open save close_file reload export_packets export_dissections quit].each { |action| item(action) } }
|
|
56
|
+
submenu(ui.t("編集")) { %i[find_packet find_next find_prev mark_packet mark_all_displayed unmark_all ignore_packet time_reference copy_filter copy_hex copy_text copy_dump copy_escaped preferences profiles].each { |action| item(action) } }
|
|
57
|
+
submenu(ui.t("表示")) do
|
|
58
|
+
%i[columns autoscroll coloring_rules toggle_coloring zoom_in zoom_out zoom_reset].each { |action| item(action) }
|
|
59
|
+
submenu(ui.t("時刻形式")) { %w[relative absolute delta delta_displayed epoch].each { |name| item(:"time_format_#{name}") } }
|
|
60
|
+
submenu(ui.t("テーマ")) { %w[system dark light high_contrast].each { |name| item(:"theme_#{name}") } }
|
|
61
|
+
end
|
|
62
|
+
submenu(ui.t("移動")) { %i[previous_packet next_packet first_packet last_packet go_to_packet prev_in_conversation next_in_conversation].each { |action| item(action) } }
|
|
63
|
+
submenu(ui.t("キャプチャ")) { %i[capture restart].each { |action| item(action) } }
|
|
64
|
+
submenu(ui.t("フィルタ")) do
|
|
42
65
|
%i[apply_filter clear_filter filter_history bookmark].each { |action| item(action) }
|
|
43
|
-
submenu("選択項目を適用") { %i[selected not and and_not or or_not].each { |mode| item(:"filter_#{mode}") } }
|
|
44
|
-
submenu("選択項目から準備") { %i[selected not and and_not or or_not].each { |mode| item(:"prepare_#{mode}") } }
|
|
66
|
+
submenu(ui.t("選択項目を適用")) { %i[selected not and and_not or or_not].each { |mode| item(:"filter_#{mode}") } }
|
|
67
|
+
submenu(ui.t("選択項目から準備")) { %i[selected not and and_not or or_not].each { |mode| item(:"prepare_#{mode}") } }
|
|
45
68
|
end
|
|
69
|
+
submenu(ui.t("分析")) { %i[follow_tcp_stream decode_as expert_info plugins reload_plugins].each { |action| item(action) } }
|
|
70
|
+
submenu(ui.t("統計")) { %i[file_properties protocol_hierarchy conversations endpoints io_graph].each { |action| item(action) } }
|
|
71
|
+
submenu(ui.t("ヘルプ")) { item(:command_palette) }
|
|
46
72
|
end
|
|
47
73
|
end
|
|
48
74
|
end
|