redhound 1.0.1 → 2.0.0.rc2

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.
Files changed (232) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +97 -31
  4. data/docs/API.md +104 -0
  5. data/docs/FILTERS.md +37 -0
  6. data/docs/MIGRATION.md +23 -0
  7. data/docs/PROTOCOLS.md +54 -0
  8. data/docs/USAGE.md +86 -0
  9. data/docs/VALIDATION.md +175 -0
  10. data/docs/json-schema.json +64 -0
  11. data/exe/redhound +2 -1
  12. data/lib/redhound/analysis/flow_key.rb +59 -0
  13. data/lib/redhound/analysis/flow_table.rb +124 -0
  14. data/lib/redhound/analysis/ip_reassembler.rb +209 -0
  15. data/lib/redhound/analysis/stats.rb +150 -0
  16. data/lib/redhound/analysis/tcp_analysis.rb +43 -0
  17. data/lib/redhound/analysis/tcp_reassembler.rb +176 -0
  18. data/lib/redhound/analysis/tcp_stream.rb +156 -0
  19. data/lib/redhound/analysis.rb +157 -0
  20. data/lib/redhound/capture/bsd/bpf_device.rb +155 -0
  21. data/lib/redhound/capture/bsd/constants.rb +29 -0
  22. data/lib/redhound/capture/bsd/ifreq.rb +41 -0
  23. data/lib/redhound/capture/file_source.rb +214 -0
  24. data/lib/redhound/capture/interface.rb +70 -0
  25. data/lib/redhound/capture/linktype.rb +30 -0
  26. data/lib/redhound/capture/linux/constants.rb +37 -0
  27. data/lib/redhound/capture/linux/cooked.rb +18 -0
  28. data/lib/redhound/capture/linux/ifreq.rb +42 -0
  29. data/lib/redhound/capture/linux/packet_socket.rb +224 -0
  30. data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
  31. data/lib/redhound/capture/linux/tpacket_v3.rb +137 -0
  32. data/lib/redhound/capture/source.rb +79 -0
  33. data/lib/redhound/capture/stats.rb +22 -0
  34. data/lib/redhound/capture.rb +70 -0
  35. data/lib/redhound/cli/command.rb +42 -0
  36. data/lib/redhound/cli/options.rb +133 -0
  37. data/lib/redhound/cli/privileges.rb +29 -0
  38. data/lib/redhound/cli/runner.rb +129 -0
  39. data/lib/redhound/context.rb +20 -0
  40. data/lib/redhound/cursor.rb +69 -0
  41. data/lib/redhound/diagnostic.rb +7 -0
  42. data/lib/redhound/dissector.rb +152 -0
  43. data/lib/redhound/engine.rb +69 -0
  44. data/lib/redhound/errors.rb +38 -0
  45. data/lib/redhound/field.rb +62 -0
  46. data/lib/redhound/file/format.rb +63 -0
  47. data/lib/redhound/file/pcap_reader.rb +43 -0
  48. data/lib/redhound/file/pcap_writer.rb +79 -0
  49. data/lib/redhound/file/pcapng_reader.rb +221 -0
  50. data/lib/redhound/file/pcapng_writer.rb +151 -0
  51. data/lib/redhound/file/rotating_writer.rb +98 -0
  52. data/lib/redhound/filter/analyzer.rb +197 -0
  53. data/lib/redhound/filter/bpf/assembler.rb +121 -0
  54. data/lib/redhound/filter/bpf/disassembler.rb +58 -0
  55. data/lib/redhound/filter/bpf/validator.rb +77 -0
  56. data/lib/redhound/filter/bpf/vm.rb +109 -0
  57. data/lib/redhound/filter/codegen.rb +413 -0
  58. data/lib/redhound/filter/lexer.rb +46 -0
  59. data/lib/redhound/filter/parser.rb +206 -0
  60. data/lib/redhound/filter/program.rb +55 -0
  61. data/lib/redhound/filter.rb +30 -0
  62. data/lib/redhound/layer.rb +75 -0
  63. data/lib/redhound/output/fields.rb +20 -0
  64. data/lib/redhound/output/follow.rb +64 -0
  65. data/lib/redhound/output/hexdump.rb +29 -0
  66. data/lib/redhound/output/json.rb +32 -0
  67. data/lib/redhound/output/summary.rb +84 -0
  68. data/lib/redhound/output/timestamp.rb +34 -0
  69. data/lib/redhound/output/tree.rb +25 -0
  70. data/lib/redhound/packet.rb +77 -0
  71. data/lib/redhound/protocols/arp.rb +39 -0
  72. data/lib/redhound/protocols/checksum.rb +38 -0
  73. data/lib/redhound/protocols/data.rb +23 -0
  74. data/lib/redhound/protocols/dhcp.rb +140 -0
  75. data/lib/redhound/protocols/dns.rb +268 -0
  76. data/lib/redhound/protocols/ethernet.rb +24 -0
  77. data/lib/redhound/protocols/http.rb +185 -0
  78. data/lib/redhound/protocols/icmp.rb +205 -0
  79. data/lib/redhound/protocols/ipv4.rb +56 -0
  80. data/lib/redhound/protocols/ipv6.rb +68 -0
  81. data/lib/redhound/protocols/link.rb +70 -0
  82. data/lib/redhound/protocols/llc.rb +37 -0
  83. data/lib/redhound/protocols/ntp.rb +45 -0
  84. data/lib/redhound/protocols/tcp.rb +88 -0
  85. data/lib/redhound/protocols/tls.rb +218 -0
  86. data/lib/redhound/protocols/tunnel.rb +48 -0
  87. data/lib/redhound/protocols/udp.rb +32 -0
  88. data/lib/redhound/protocols/vlan.rb +23 -0
  89. data/lib/redhound/reader.rb +8 -0
  90. data/lib/redhound/registry.rb +64 -0
  91. data/lib/redhound/stream_dissector.rb +168 -0
  92. data/lib/redhound/util/seq.rb +21 -0
  93. data/lib/redhound/version.rb +2 -1
  94. data/lib/redhound/writer.rb +25 -43
  95. data/lib/redhound.rb +81 -9
  96. data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
  97. data/sig/generated/redhound/analysis/flow_table.rbs +102 -0
  98. data/sig/generated/redhound/analysis/ip_reassembler.rbs +52 -0
  99. data/sig/generated/redhound/analysis/stats.rbs +80 -0
  100. data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
  101. data/sig/generated/redhound/analysis/tcp_reassembler.rbs +39 -0
  102. data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
  103. data/sig/generated/redhound/analysis.rbs +38 -0
  104. data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
  105. data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
  106. data/sig/generated/redhound/capture/bsd/ifreq.rbs +15 -0
  107. data/sig/generated/redhound/capture/file_source.rbs +67 -0
  108. data/sig/generated/redhound/capture/interface.rbs +53 -0
  109. data/sig/generated/redhound/capture/linktype.rbs +20 -0
  110. data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
  111. data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
  112. data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
  113. data/sig/generated/redhound/capture/linux/packet_socket.rbs +64 -0
  114. data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
  115. data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
  116. data/sig/generated/redhound/capture/source.rbs +44 -0
  117. data/sig/generated/redhound/capture/stats.rbs +25 -0
  118. data/sig/generated/redhound/capture.rbs +14 -0
  119. data/sig/generated/redhound/cli/command.rbs +12 -0
  120. data/sig/generated/redhound/cli/options.rbs +25 -0
  121. data/sig/generated/redhound/cli/privileges.rbs +12 -0
  122. data/sig/generated/redhound/cli/runner.rbs +27 -0
  123. data/sig/generated/redhound/context.rbs +27 -0
  124. data/sig/generated/redhound/cursor.rbs +59 -0
  125. data/sig/generated/redhound/diagnostic.rbs +21 -0
  126. data/sig/generated/redhound/dissector.rbs +117 -0
  127. data/sig/generated/redhound/engine.rbs +23 -0
  128. data/sig/generated/redhound/errors.rbs +56 -0
  129. data/sig/generated/redhound/field.rbs +74 -0
  130. data/sig/generated/redhound/file/format.rbs +39 -0
  131. data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
  132. data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
  133. data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
  134. data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
  135. data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
  136. data/sig/generated/redhound/filter/analyzer.rbs +42 -0
  137. data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
  138. data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
  139. data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
  140. data/sig/generated/redhound/filter/bpf/vm.rbs +28 -0
  141. data/sig/generated/redhound/filter/codegen.rbs +101 -0
  142. data/sig/generated/redhound/filter/lexer.rbs +15 -0
  143. data/sig/generated/redhound/filter/parser.rbs +65 -0
  144. data/sig/generated/redhound/filter/program.rbs +48 -0
  145. data/sig/generated/redhound/filter.rbs +10 -0
  146. data/sig/generated/redhound/layer.rbs +81 -0
  147. data/sig/generated/redhound/output/fields.rbs +15 -0
  148. data/sig/generated/redhound/output/follow.rbs +21 -0
  149. data/sig/generated/redhound/output/hexdump.rbs +15 -0
  150. data/sig/generated/redhound/output/json.rbs +18 -0
  151. data/sig/generated/redhound/output/summary.rbs +21 -0
  152. data/sig/generated/redhound/output/timestamp.rbs +21 -0
  153. data/sig/generated/redhound/output/tree.rbs +15 -0
  154. data/sig/generated/redhound/packet.rbs +82 -0
  155. data/sig/generated/redhound/protocols/arp.rbs +15 -0
  156. data/sig/generated/redhound/protocols/checksum.rbs +15 -0
  157. data/sig/generated/redhound/protocols/data.rbs +17 -0
  158. data/sig/generated/redhound/protocols/dhcp.rbs +31 -0
  159. data/sig/generated/redhound/protocols/dns.rbs +42 -0
  160. data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
  161. data/sig/generated/redhound/protocols/http.rbs +36 -0
  162. data/sig/generated/redhound/protocols/icmp.rbs +41 -0
  163. data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
  164. data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
  165. data/sig/generated/redhound/protocols/link.rbs +33 -0
  166. data/sig/generated/redhound/protocols/llc.rbs +15 -0
  167. data/sig/generated/redhound/protocols/ntp.rbs +17 -0
  168. data/sig/generated/redhound/protocols/tcp.rbs +25 -0
  169. data/sig/generated/redhound/protocols/tls.rbs +51 -0
  170. data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
  171. data/sig/generated/redhound/protocols/udp.rbs +18 -0
  172. data/sig/generated/redhound/protocols/vlan.rbs +12 -0
  173. data/sig/generated/redhound/reader.rbs +7 -0
  174. data/sig/generated/redhound/registry.rbs +45 -0
  175. data/sig/generated/redhound/stream_dissector.rbs +44 -0
  176. data/sig/generated/redhound/util/seq.rbs +20 -0
  177. data/sig/generated/redhound/version.rbs +1 -0
  178. data/sig/generated/redhound/writer.rbs +6 -19
  179. data/sig/generated/redhound.rbs +18 -0
  180. data/sig/shims/io.rbs +7 -0
  181. data/sig/shims/process.rbs +3 -0
  182. metadata +177 -55
  183. data/CODE_OF_CONDUCT.md +0 -132
  184. data/Dockerfile +0 -7
  185. data/Rakefile +0 -15
  186. data/Steepfile +0 -4
  187. data/docker-compose.yml +0 -9
  188. data/lib/redhound/analyzer.rb +0 -34
  189. data/lib/redhound/builder/packet_mreq.rb +0 -56
  190. data/lib/redhound/builder/socket.rb +0 -35
  191. data/lib/redhound/builder.rb +0 -4
  192. data/lib/redhound/command.rb +0 -67
  193. data/lib/redhound/l2/ether.rb +0 -68
  194. data/lib/redhound/l2/protocol.rb +0 -33
  195. data/lib/redhound/l2.rb +0 -4
  196. data/lib/redhound/l3/arp.rb +0 -114
  197. data/lib/redhound/l3/base.rb +0 -49
  198. data/lib/redhound/l3/ipv4.rb +0 -95
  199. data/lib/redhound/l3/ipv6.rb +0 -69
  200. data/lib/redhound/l3/protocol.rb +0 -173
  201. data/lib/redhound/l3/resolver.rb +0 -30
  202. data/lib/redhound/l3.rb +0 -9
  203. data/lib/redhound/l4/base.rb +0 -31
  204. data/lib/redhound/l4/icmp.rb +0 -71
  205. data/lib/redhound/l4/resolver.rb +0 -28
  206. data/lib/redhound/l4/udp.rb +0 -63
  207. data/lib/redhound/l4.rb +0 -6
  208. data/lib/redhound/receiver.rb +0 -45
  209. data/lib/redhound/resolver.rb +0 -22
  210. data/lib/redhound/source/socket.rb +0 -18
  211. data/lib/redhound/source.rb +0 -3
  212. data/rbs_collection.lock.yaml +0 -20
  213. data/rbs_collection.yaml +0 -17
  214. data/sig/generated/redhound/analyzer.rbs +0 -14
  215. data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
  216. data/sig/generated/redhound/builder/socket.rbs +0 -24
  217. data/sig/generated/redhound/command.rbs +0 -19
  218. data/sig/generated/redhound/l2/ether.rbs +0 -41
  219. data/sig/generated/redhound/l2/protocol.rbs +0 -15
  220. data/sig/generated/redhound/l3/arp.rbs +0 -57
  221. data/sig/generated/redhound/l3/base.rbs +0 -28
  222. data/sig/generated/redhound/l3/ipv4.rbs +0 -53
  223. data/sig/generated/redhound/l3/ipv6.rbs +0 -38
  224. data/sig/generated/redhound/l3/protocol.rbs +0 -16
  225. data/sig/generated/redhound/l3/resolver.rbs +0 -16
  226. data/sig/generated/redhound/l4/base.rbs +0 -19
  227. data/sig/generated/redhound/l4/icmp.rbs +0 -33
  228. data/sig/generated/redhound/l4/resolver.rbs +0 -16
  229. data/sig/generated/redhound/l4/udp.rbs +0 -36
  230. data/sig/generated/redhound/receiver.rbs +0 -19
  231. data/sig/generated/redhound/resolver.rbs +0 -11
  232. data/sig/generated/redhound/source/socket.rbs +0 -13
@@ -0,0 +1,140 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Dhcp < Dissector
9
+ MESSAGE_TYPES = { 1 => 'Discover', 2 => 'Offer', 3 => 'Request', 4 => 'Decline', 5 => 'ACK',
10
+ 6 => 'NAK', 7 => 'Release', 8 => 'Inform' }.freeze
11
+ ADDRESS_OPTIONS = { 1 => 'subnet_mask', 3 => 'router', 6 => 'domain_name_server',
12
+ 50 => 'requested_ip_address', 54 => 'dhcp_server_id' }.freeze
13
+ protocol :dhcp, name: 'Dynamic Host Configuration Protocol', short: 'DHCP'
14
+ dissects_on 'udp.port', 67, 68
15
+ header do
16
+ uint8 :type, 'dhcp.type'
17
+ uint8 :hw_type, 'dhcp.hw.type'
18
+ uint8 :hw_len, 'dhcp.hw.len'
19
+ uint8 :hops, 'dhcp.hops'
20
+ uint32 :xid, 'dhcp.id', format: :hex
21
+ uint16 :secs, 'dhcp.secs'
22
+ uint16 :flags, 'dhcp.flags', format: :hex
23
+ ipv4 :client, 'dhcp.ip.client'
24
+ ipv4 :your, 'dhcp.ip.your'
25
+ ipv4 :server, 'dhcp.ip.server'
26
+ ipv4 :relay, 'dhcp.ip.relay'
27
+ end
28
+
29
+ # @rbs (Context ctx, Layer layer) -> void
30
+ def dissect(ctx, layer)
31
+ cursor = ctx.cursor
32
+ cursor.check(0, 236)
33
+ return layer.diagnose(:error, :bad_length, 'hardware address is longer than 16 bytes') if layer[:hw_len] > 16
34
+ put(layer, 'dhcp.hw.mac_addr', cursor.bytes(28, layer[:hw_len]),
35
+ layer[:hw_type] == 1 && layer[:hw_len] == 6 ? :mac : :bytes, 28, layer[:hw_len])
36
+ put(layer, 'dhcp.server', cursor.bytes(44, 64).split("\0", 2).first || '', :string, 44, 64)
37
+ put(layer, 'dhcp.file', cursor.bytes(108, 128).split("\0", 2).first || '', :string, 108, 128)
38
+ return if cursor.remaining == 236
39
+ cursor.check(236, 4)
40
+ unless cursor.u32(236) == 0x63825363
41
+ put(layer, 'dhcp.vendor', cursor.bytes(236, cursor.remaining - 236), :bytes, 236, cursor.remaining - 236)
42
+ return
43
+ end
44
+ put(layer, 'dhcp.cookie', 0x63825363, :uint, 236, 4)
45
+ # @type var options: Hash[Integer, untyped]
46
+ options = Hash.new
47
+ read_options(cursor, layer, 240, cursor.remaining, options)
48
+ overload = options[52]&.map { |entry| entry[0] }&.join
49
+ if overload
50
+ return layer.diagnose(:error, :bad_length, 'invalid DHCP overload option') unless overload.bytesize == 1 && overload.getbyte(0).between?(1, 3)
51
+ read_options(cursor, layer, 108, 236, options) if overload.getbyte(0) & 1 != 0
52
+ read_options(cursor, layer, 44, 108, options) if overload.getbyte(0) & 2 != 0
53
+ end
54
+ options.each do |code, entries|
55
+ value = entries.map { |entry| entry[0] }.join
56
+ decode_option(layer, code, value, entries)
57
+ end
58
+ rescue Cursor::Truncated => e
59
+ layer.diagnose(:note, :truncated, e.message)
60
+ ensure
61
+ layer.header_length = cursor.remaining
62
+ layer.payload_offset = layer.payload_end
63
+ end
64
+
65
+ # @rbs (Cursor cursor, Layer layer, Integer pos, Integer ending, untyped options) -> void
66
+ def read_options(cursor, layer, pos, ending, options)
67
+ while pos < ending
68
+ code = cursor.u8(pos)
69
+ pos += 1
70
+ next if code.zero?
71
+ break if code == 255
72
+ raise Cursor::Truncated, 'truncated DHCP option length' if pos >= ending
73
+ len = cursor.u8(pos)
74
+ pos += 1
75
+ raise Cursor::Truncated, 'DHCP option exceeds available bytes' if pos + len > ending
76
+ value = cursor.bytes(pos, len)
77
+ put(layer, 'dhcp.option.type', code, :uint, pos - 2, 1)
78
+ put(layer, 'dhcp.option.length', len, :uint, pos - 1, 1)
79
+ put(layer, 'dhcp.option.value', value, :bytes, pos, len)
80
+ options[code] = Array.new unless options.key?(code)
81
+ options[code] << [value, pos]
82
+ pos += len
83
+ end
84
+ end
85
+
86
+ # @rbs (Layer layer, Integer code, String value, untyped entries) -> void
87
+ def decode_option(layer, code, value, entries)
88
+ offset = entries.first[1]
89
+ length = value.bytesize
90
+ if ADDRESS_OPTIONS.key?(code)
91
+ valid = length.positive? && length % 4 == 0 && (![1, 50, 54].include?(code) || length == 4)
92
+ return layer.diagnose(:error, :bad_length, "invalid DHCP option #{code} length") unless valid
93
+ (length / 4).times do |i|
94
+ put(layer, "dhcp.option.#{ADDRESS_OPTIONS[code]}", value.unpack1('N', offset: i * 4), :ipv4, offset + i * 4, 4, entries)
95
+ end
96
+ else
97
+ case code
98
+ when 53, 52
99
+ return layer.diagnose(:error, :bad_length, "invalid DHCP option #{code} length") unless length == 1
100
+ put(layer, code == 53 ? 'dhcp.option.dhcp' : 'dhcp.option.overload', value.getbyte(0), :uint, offset, 1, entries)
101
+ when 51
102
+ return layer.diagnose(:error, :bad_length, 'invalid DHCP lease length') unless length == 4
103
+ put(layer, 'dhcp.option.ip_address_lease_time', value.unpack1('N'), :uint, offset, 4, entries)
104
+ when 55
105
+ length.times { |i| put(layer, 'dhcp.option.request_list_item', value.getbyte(i), :uint, offset + i, 1, entries) }
106
+ when 61
107
+ return layer.diagnose(:error, :bad_length, 'DHCP client ID must include type and identifier') if length < 2
108
+ put(layer, 'dhcp.option.client_id', value, :bytes, offset, length, entries)
109
+ put(layer, 'dhcp.option.client_id.type', value.getbyte(0), :uint, offset, 1, entries)
110
+ put(layer, 'dhcp.option.client_id.hw_mac_addr', value.byteslice(1, 6), :mac, offset + 1, 6, entries) if length == 7 && value.getbyte(0) == 1
111
+ when 12 then put(layer, 'dhcp.option.hostname', value, :string, offset, length, entries)
112
+ end
113
+ end
114
+ end
115
+
116
+ # @rbs (untyped entries, Integer logical) -> Integer
117
+ def wire_offset(entries, logical)
118
+ entries.each do |bytes, pos|
119
+ return pos + logical if logical < bytes.bytesize
120
+ logical -= bytes.bytesize
121
+ end
122
+ entries.last[1] + entries.last[0].bytesize
123
+ end
124
+
125
+ # @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length, ?untyped entries) -> void
126
+ def put(layer, name, value, type, offset, length, entries = nil)
127
+ if entries
128
+ logical = offset - entries.first[1]
129
+ first = wire_offset(entries, logical)
130
+ ending = length.zero? ? first : wire_offset(entries, logical + length - 1) + 1
131
+ offset, length = first, ending - first
132
+ end
133
+ layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
134
+ end
135
+
136
+ # @rbs (Layer layer) -> String
137
+ def summary(layer) = "DHCP #{MESSAGE_TYPES[layer.field_value('dhcp.option.dhcp')] || 'BOOTP'}, xid #{format('0x%x', layer[:xid] || 0)}"
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,268 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Dns < StreamDissector
9
+ # @api private
10
+ class Malformed < StandardError; end
11
+ TYPES = { 1 => 'A', 2 => 'NS', 5 => 'CNAME', 6 => 'SOA', 12 => 'PTR', 15 => 'MX',
12
+ 16 => 'TXT', 28 => 'AAAA', 33 => 'SRV', 41 => 'OPT', 64 => 'SVCB', 65 => 'HTTPS' }.freeze
13
+ protocol :dns, name: 'Domain Name System', short: 'DNS'
14
+ dissects_on 'udp.port', 53, 5353, 5355
15
+ dissects_on 'tcp.port', 53, 5353, 5355
16
+
17
+ # @rbs (Context ctx, Layer layer) -> void
18
+ def dissect(ctx, layer)
19
+ cursor = ctx.cursor
20
+ pos = 0
21
+ transport = ctx.layers.last
22
+ tcp = transport&.protocol == :tcp
23
+ port = [transport&.[](:srcport), transport&.[](:dstport)].compact.sort.find { |value| [53, 5353, 5355].include?(value) }
24
+ variant = { 5353 => :mdns, 5355 => :llmnr }.fetch(port, :dns)
25
+ layer.values[:variant] = variant
26
+ loop do
27
+ if tcp
28
+ len = cursor.u16(pos)
29
+ put(layer, 'dns.length', len, :uint, pos, 2)
30
+ raise Malformed, 'DNS message shorter than header' if len < 12
31
+ pos += 2
32
+ ending = pos + len
33
+ layer.diagnose(:note, :truncated) if ending > cursor.remaining
34
+ message(cursor.sub(cursor.start + pos, cursor.start + ending), layer, variant)
35
+ pos = ending
36
+ else
37
+ message(cursor, layer, variant)
38
+ pos = cursor.remaining
39
+ end
40
+ break unless tcp && pos < cursor.remaining
41
+ end
42
+ rescue Cursor::Truncated => e
43
+ layer.diagnose(:note, :truncated, e.message)
44
+ rescue Malformed => e
45
+ layer.diagnose(:error, :malformed, e.message)
46
+ ensure
47
+ layer.header_length = cursor.remaining
48
+ layer.payload_offset = layer.payload_end
49
+ end
50
+
51
+ # @rbs (Cursor cursor, Layer layer, Symbol variant) -> void
52
+ def message(cursor, layer, variant)
53
+ cursor.check(0, 12)
54
+ base = cursor.start - layer.offset
55
+ put(layer, 'dns.id', cursor.u16(0), :uint, base, 2)
56
+ flags = cursor.u16(2)
57
+ put(layer, 'dns.flags', flags, :uint, base + 2, 2)
58
+ flag_bits = { response: [15, 1], opcode: [11, 15], authoritative: [10, 1], truncated: [9, 1],
59
+ recdesired: [8, 1], recavail: [7, 1], authenticated: [5, 1], checkdisable: [4, 1], rcode: [0, 15] }
60
+ if variant == :llmnr
61
+ %i[authoritative recdesired recavail authenticated checkdisable].each { |key| flag_bits.delete(key) }
62
+ flag_bits.merge!(conflict: [10, 1], tentative: [8, 1])
63
+ end
64
+ flag_bits.each do |key, pair|
65
+ next if key == :rcode && flags & 0x8000 == 0
66
+ shift, mask = pair.fetch(0), pair.fetch(1)
67
+ value = (flags >> shift) & mask
68
+ put(layer, "dns.flags.#{key}", mask == 1 ? value == 1 : value, mask == 1 ? :boolean : :uint, base + 2, 2)
69
+ end
70
+ counts = cursor.unpack('n4', 8, 4)
71
+ %w[queries answers auth_rr additional_rr].each_with_index do |name, i|
72
+ put(layer, "dns.count.#{name}", counts[i], :uint, base + 4 + i * 2, 2)
73
+ end
74
+ pos = 12
75
+ counts[0].times do
76
+ name, after = read_name(cursor, pos)
77
+ put(layer, 'dns.qry.name', name, :string, base + pos, after - pos)
78
+ cursor.check(after, 4)
79
+ put(layer, 'dns.qry.type', cursor.u16(after), :uint, base + after, 2)
80
+ klass = cursor.u16(after + 2)
81
+ put(layer, 'dns.qry.class', variant == :mdns ? klass & 0x7fff : klass, :uint, base + after + 2, 2)
82
+ put(layer, 'dns.qry.qu', klass & 0x8000 != 0, :boolean, base + after + 2, 2) if variant == :mdns
83
+ pos = after + 4
84
+ end
85
+ counts.drop(1).sum.times { pos = record(cursor, layer, pos, base, variant) }
86
+ end
87
+
88
+ # @rbs (Cursor cursor, Integer pos, ?Integer? limit, ?bool compression) -> [String, Integer]
89
+ def read_name(cursor, pos, limit = nil, compression = true)
90
+ limit ||= cursor.remaining
91
+ labels, seen, wire_length, jumps, ending = [], {}, 1, 0, nil
92
+ loop do
93
+ raise Malformed, 'DNS name compression loop' if seen[pos]
94
+ seen[pos] = true
95
+ len = cursor.u8(pos)
96
+ raise Malformed, 'DNS name exceeds record length' if ending.nil? && pos >= limit
97
+ if len & 0xc0 == 0xc0
98
+ raise Malformed, 'compressed target name is forbidden' unless compression
99
+ raise Malformed, 'DNS pointer exceeds record length' if ending.nil? && pos + 2 > limit
100
+ target = cursor.u16(pos) & 0x3fff
101
+ raise Malformed, 'DNS pointer is not backward' unless target < pos
102
+ jumps += 1
103
+ raise Malformed, 'DNS compression exceeds 128 jumps' if jumps > 128
104
+ ending ||= pos + 2
105
+ pos = target
106
+ else
107
+ raise Malformed, 'invalid DNS label type' unless len & 0xc0 == 0
108
+ pos += 1
109
+ break if len.zero?
110
+ wire_length += len + 1
111
+ raise Malformed, 'DNS name exceeds 255 bytes' if wire_length > 255
112
+ cursor.check(pos, len)
113
+ raise Malformed, 'DNS label exceeds record length' if ending.nil? && pos + len > limit
114
+ labels << cursor.bytes(pos, len)
115
+ pos += len
116
+ end
117
+ end
118
+ [labels.join('.'), ending || pos]
119
+ end
120
+
121
+ # @rbs (Cursor cursor, Layer layer, Integer pos, Integer base, Symbol variant) -> Integer
122
+ def record(cursor, layer, pos, base, variant)
123
+ name, after = read_name(cursor, pos)
124
+ cursor.check(after, 10)
125
+ type, klass, ttl, length = cursor.unpack('nnNn', 10, after)
126
+ first, ending = after + 10, after + 10 + length
127
+ cursor.check(first, length)
128
+ if type == 33
129
+ service, protocol, domain = name.split('.', 3)
130
+ put(layer, 'dns.srv.service', service || '', :string, base + pos, after - pos)
131
+ put(layer, 'dns.srv.proto', protocol || '', :string, base + pos, after - pos)
132
+ put(layer, 'dns.srv.name', domain, :string, base + pos, after - pos) if domain
133
+ else
134
+ put(layer, 'dns.resp.name', name.empty? ? '<Root>' : name, :string, base + pos, after - pos)
135
+ end
136
+ put(layer, 'dns.resp.type', type, :uint, base + after, 2)
137
+ put(layer, 'dns.resp.len', length, :uint, base + after + 8, 2)
138
+ if type == 41
139
+ put(layer, 'dns.rr.udp_payload_size', klass, :uint, base + after + 2, 2)
140
+ put(layer, 'dns.resp.ext_rcode', ttl >> 24, :uint, base + after + 4, 1)
141
+ put(layer, 'dns.resp.edns0_version', (ttl >> 16) & 255, :uint, base + after + 5, 1)
142
+ put(layer, 'dns.resp.z', ttl & 0xffff, :uint, base + after + 6, 2)
143
+ put(layer, 'dns.resp.z.do', ttl & 0x8000 != 0, :boolean, base + after + 6, 2)
144
+ edns(cursor, layer, first, ending, base)
145
+ else
146
+ put(layer, 'dns.resp.class', variant == :mdns ? klass & 0x7fff : klass, :uint, base + after + 2, 2)
147
+ put(layer, 'dns.resp.ttl', ttl, :uint, base + after + 4, 4)
148
+ put(layer, 'dns.resp.cache_flush', klass & 0x8000 != 0, :boolean, base + after + 2, 2) if variant == :mdns
149
+ rdata(cursor, layer, type, first, ending, base)
150
+ end
151
+ ending
152
+ end
153
+
154
+ # @rbs (Cursor cursor, Layer layer, Integer type, Integer pos, Integer ending, Integer base) -> void
155
+ def rdata(cursor, layer, type, pos, ending, base)
156
+ length = ending - pos
157
+ case type
158
+ when 1, 28
159
+ expected = type == 1 ? 4 : 16
160
+ return layer.diagnose(:error, :bad_length, 'invalid DNS address length') unless length == expected
161
+ value = type == 1 ? cursor.u32(pos) : cursor.bytes(pos, 16)
162
+ put(layer, type == 1 ? 'dns.a' : 'dns.aaaa', value, type == 1 ? :ipv4 : :ipv6, base + pos, length)
163
+ when 2, 5, 12
164
+ name, after = read_name(cursor, pos, ending)
165
+ put(layer, { 2 => 'dns.ns', 5 => 'dns.cname', 12 => 'dns.ptr.domain_name' }.fetch(type), name, :string, base + pos, after - pos)
166
+ raise Malformed, 'DNS name has trailing record bytes' unless after == ending
167
+ when 15, 33
168
+ width = type == 15 ? 2 : 6
169
+ raise Malformed, 'DNS service record is too short' if length < width + 1
170
+ names = type == 15 ? ['dns.mx.preference'] : %w[dns.srv.priority dns.srv.weight dns.srv.port]
171
+ names.each_with_index { |name, i| put(layer, name, cursor.u16(pos + i * 2), :uint, base + pos + i * 2, 2) }
172
+ name, after = read_name(cursor, pos + width, ending)
173
+ put(layer, type == 15 ? 'dns.mx.mail_exchange' : 'dns.srv.target', name, :string, base + pos + width, after - pos - width)
174
+ raise Malformed, 'DNS service record has trailing bytes' unless after == ending
175
+ when 16
176
+ while pos < ending
177
+ len = cursor.u8(pos)
178
+ raise Malformed, 'DNS TXT string exceeds record' if pos + 1 + len > ending
179
+ put(layer, 'dns.txt', cursor.bytes(pos + 1, len), :string, base + pos + 1, len)
180
+ pos += len + 1
181
+ end
182
+ when 6
183
+ %w[mname rname].each do |key|
184
+ name, after = read_name(cursor, pos, ending)
185
+ put(layer, "dns.soa.#{key}", name, :string, base + pos, after - pos)
186
+ pos = after
187
+ end
188
+ raise Malformed, 'invalid SOA integer section' unless ending - pos == 20
189
+ %w[serial_number refresh_interval retry_interval expire_limit minimum_ttl].each_with_index do |key, i|
190
+ put(layer, "dns.soa.#{key}", cursor.u32(pos + i * 4), :uint, base + pos + i * 4, 4)
191
+ end
192
+ when 64, 65 then service_binding(cursor, layer, pos, ending, base)
193
+ else put(layer, 'dns.data', cursor.bytes(pos, length), :bytes, base + pos, length)
194
+ end
195
+ end
196
+
197
+ # @rbs (Cursor cursor, Layer layer, Integer pos, Integer ending, Integer base) -> void
198
+ def service_binding(cursor, layer, pos, ending, base)
199
+ raise Malformed, 'short SVCB record' if ending - pos < 3
200
+ priority = cursor.u16(pos)
201
+ put(layer, 'dns.svcb.svcpriority', priority, :uint, base + pos, 2)
202
+ name, after = read_name(cursor, pos + 2, ending, false)
203
+ put(layer, 'dns.svcb.targetname', name, :string, base + pos + 2, after - pos - 2)
204
+ pos, previous = after, -1
205
+ while pos < ending
206
+ raise Malformed, 'short SVCB parameter header' if ending - pos < 4
207
+ key, len = cursor.u16(pos), cursor.u16(pos + 2)
208
+ raise Malformed, 'invalid SVCB parameter order or length' if key <= previous || pos + 4 + len > ending
209
+ previous = key
210
+ put(layer, 'dns.svcb.svcparam.key', key, :uint, base + pos, 2)
211
+ put(layer, 'dns.svcb.svcparam.value.length', len, :uint, base + pos + 2, 2)
212
+ first = pos + 4
213
+ case key
214
+ when 0, 3, 4, 6
215
+ size = { 0 => 2, 3 => 2, 4 => 4, 6 => 16 }.fetch(key)
216
+ raise Malformed, 'invalid SVCB parameter size' if len.zero? || len % size != 0 || (key == 3 && len != 2)
217
+ (len / size).times do |i|
218
+ value = size == 16 ? cursor.bytes(first + i * size, size) : (size == 4 ? cursor.u32(first + i * size) : cursor.u16(first + i * size))
219
+ name = { 0 => 'mandatory.key', 3 => 'port', 4 => 'ipv4hint.ip', 6 => 'ipv6hint.ip' }.fetch(key)
220
+ put(layer, "dns.svcb.svcparam.#{name}", value, { 4 => :ipv4, 6 => :ipv6 }.fetch(key, :uint), base + first + i * size, size)
221
+ end
222
+ when 1
223
+ while first < pos + 4 + len
224
+ size = cursor.u8(first)
225
+ raise Malformed, 'invalid SVCB ALPN size' if size.zero? || first + size + 1 > pos + 4 + len
226
+ put(layer, 'dns.svcb.svcparam.alpn', cursor.bytes(first + 1, size), :string, base + first + 1, size)
227
+ first += size + 1
228
+ end
229
+ when 2 then raise Malformed, 'no-default-alpn must be empty' unless len.zero?
230
+ else put(layer, key == 7 ? 'dns.svcb.svcparam.dohpath' : 'dns.svcb.svcparam.value', cursor.bytes(first, len), key == 7 ? :string : :bytes, base + first, len)
231
+ end
232
+ pos += len + 4
233
+ end
234
+ end
235
+
236
+ # @rbs (Cursor cursor, Layer layer, Integer pos, Integer ending, Integer base) -> void
237
+ def edns(cursor, layer, pos, ending, base)
238
+ while pos < ending
239
+ raise Malformed, 'short EDNS option header' if ending - pos < 4
240
+ code, len = cursor.u16(pos), cursor.u16(pos + 2)
241
+ raise Malformed, 'EDNS option exceeds record' if pos + 4 + len > ending
242
+ put(layer, 'dns.opt.code', code, :uint, base + pos, 2)
243
+ put(layer, 'dns.opt.len', len, :uint, base + pos + 2, 2)
244
+ put(layer, 'dns.opt.data', cursor.bytes(pos + 4, len), :bytes, base + pos + 4, len)
245
+ if code == 10
246
+ raise Malformed, 'invalid EDNS cookie length' unless len == 8 || len.between?(16, 40)
247
+ put(layer, 'dns.opt.cookie.client', cursor.bytes(pos + 4, 8), :bytes, base + pos + 4, 8)
248
+ put(layer, 'dns.opt.cookie.server', cursor.bytes(pos + 12, len - 8), :bytes, base + pos + 12, len - 8) if len > 8
249
+ end
250
+ pos += len + 4
251
+ end
252
+ end
253
+
254
+ # @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length) -> void
255
+ def put(layer, name, value, type, offset, length)
256
+ layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
257
+ end
258
+
259
+ # @rbs (Layer layer) -> String
260
+ def summary(layer)
261
+ name = layer.fields.find { |field| field.name == 'dns.qry.name' }&.display || ''
262
+ type = TYPES[layer.field_value('dns.qry.type')] || layer.field_value('dns.qry.type')
263
+ label = { mdns: 'mDNS', llmnr: 'LLMNR' }.fetch(layer[:variant], 'DNS')
264
+ "#{label} #{layer.field_value('dns.flags.response') ? 'response' : 'query'} #{type} #{name}"
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,24 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Ethernet < Dissector
9
+ protocol :eth, name: 'Ethernet', short: 'ETH'
10
+ dissects_on 'linktype', 1
11
+ header do
12
+ mac :dst, 'eth.dst'
13
+ mac :src, 'eth.src'
14
+ uint16 :type, 'eth.type', format: :hex
15
+ end
16
+ # @rbs (Context ctx, Layer layer) -> void
17
+ def dissect(ctx, layer)
18
+ layer.payload_end = layer.payload_offset + layer[:type] if layer[:type] <= 1500
19
+ end
20
+ # @rbs (Context ctx, Layer layer) -> untyped
21
+ def next_dissector(ctx, layer) = layer[:type] <= 1500 ? Llc : ctx.registry.lookup('ethertype', layer[:type])
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,185 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Http < StreamDissector
9
+ HEADER_LIMIT = 65_536
10
+ TOKEN = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/n
11
+ HEADERS = %w[host user_agent accept content_type content_length transfer_encoding connection
12
+ server location referer cookie set_cookie authorization cache_control].freeze
13
+ protocol :http, name: 'Hypertext Transfer Protocol', short: 'HTTP'
14
+ dissects_on 'tcp.port', 80, 8080, 8000
15
+
16
+ # @rbs (Context ctx, Cursor cursor) -> bool
17
+ def self.heuristic?(ctx, cursor)
18
+ sample = cursor.bytes(0, [cursor.remaining, 128].min)
19
+ sample.match?(/\A(?:HTTP\/1\.[01] |[A-Z]{3,20} \S+ HTTP\/1\.[01](?:\r\n|\z))/n)
20
+ end
21
+
22
+ # @rbs (Context ctx, Layer layer) -> void
23
+ def dissect(ctx, layer)
24
+ bytes = ctx.cursor.bytes(0, ctx.cursor.remaining)
25
+ pos = 0
26
+ while pos < bytes.bytesize
27
+ first = bytes.index("\r\n", pos)
28
+ return layer.diagnose(:note, :truncated, 'incomplete HTTP start line') unless first
29
+ return layer.diagnose(:error, :malformed, 'HTTP start line is too long') if first - pos > HEADER_LIMIT
30
+ line = bytes.byteslice(pos, first - pos)
31
+ response = line.start_with?('HTTP/')
32
+ status = start_line(layer, line, pos, response)
33
+ return if layer.error?
34
+ ending = bytes.index("\r\n\r\n", pos)
35
+ return layer.diagnose(:note, :truncated, 'incomplete HTTP headers') unless ending
36
+ return layer.diagnose(:error, :malformed, 'HTTP headers exceed 64 KiB') if ending - pos > HEADER_LIMIT
37
+ headers = parse_headers(layer, bytes, first + 2, ending + 2)
38
+ return if layer.error?
39
+ body_start = ending + 4
40
+ length_values = headers.fetch('content-length', []).flat_map { |value| value.empty? ? [''] : value.split(',', -1).map(&:strip) }
41
+ unless length_values.all? { |value| value.match?(/\A[0-9]+\z/n) } && length_values.uniq.length <= 1
42
+ return layer.diagnose(:error, :malformed, 'conflicting or invalid Content-Length')
43
+ end
44
+ content_length = length_values.first&.to_i
45
+ transfer = headers.fetch('transfer-encoding', []).join(',').downcase.split(',').map(&:strip)
46
+ if !transfer.empty? && content_length
47
+ return layer.diagnose(:error, :malformed, 'Transfer-Encoding and Content-Length conflict')
48
+ end
49
+ no_body = response && (ctx.packet.meta[:http_head_response] == true || status.between?(100, 199) || [204, 304].include?(status))
50
+ if no_body
51
+ pos = body_start
52
+ elsif transfer.last == 'chunked'
53
+ body, pos = chunked(layer, bytes, body_start)
54
+ put(layer, 'http.file_data', body, :bytes, body_start, pos - body_start) unless body.empty?
55
+ elsif !transfer.empty?
56
+ return layer.diagnose(:error, :malformed, 'request transfer coding must end in chunked') unless response
57
+ put(layer, 'http.file_data', bytes.byteslice(body_start..), :bytes, body_start, bytes.bytesize - body_start)
58
+ pos = bytes.bytesize
59
+ elsif content_length
60
+ available = [content_length, bytes.bytesize - body_start].min
61
+ put(layer, 'http.file_data', bytes.byteslice(body_start, available), :bytes, body_start, available) if available.positive?
62
+ pos = body_start + available
63
+ layer.diagnose(:note, :truncated, 'incomplete HTTP body') if available < content_length
64
+ elsif response
65
+ put(layer, 'http.file_data', bytes.byteslice(body_start..), :bytes, body_start, bytes.bytesize - body_start) if body_start < bytes.bytesize
66
+ pos = bytes.bytesize
67
+ else
68
+ pos = body_start
69
+ end
70
+ break if layer.error?
71
+ end
72
+ ensure
73
+ layer.header_length = ctx.cursor.remaining
74
+ layer.payload_offset = layer.payload_end
75
+ end
76
+
77
+ # @rbs (Layer layer, String line, Integer offset, bool response) -> Integer
78
+ def start_line(layer, line, offset, response)
79
+ if response
80
+ match = /\A(HTTP\/1\.[01]) ([0-9]{3})(?: (.*))?\z/n.match(line)
81
+ return layer.diagnose(:error, :malformed, 'invalid HTTP status line') && 0 unless match
82
+ put(layer, 'http.response.version', match[1], :string, offset, match[1].bytesize)
83
+ put(layer, 'http.response.code', match[2].to_i, :uint, offset + match[1].bytesize + 1, 3)
84
+ put(layer, 'http.response.phrase', match[3] || '', :string, offset + match[1].bytesize + 5, (match[3] || '').bytesize)
85
+ match[2].to_i
86
+ else
87
+ match = /\A([^ ]+) ([^ ]+) (HTTP\/1\.[01])\z/n.match(line)
88
+ return layer.diagnose(:error, :malformed, 'invalid HTTP request line') && 0 unless match && match[1].match?(TOKEN)
89
+ put(layer, 'http.request.method', match[1], :string, offset, match[1].bytesize)
90
+ put(layer, 'http.request.uri', match[2], :string, offset + match[1].bytesize + 1, match[2].bytesize)
91
+ put(layer, 'http.request.version', match[3], :string, offset + line.bytesize - match[3].bytesize, match[3].bytesize)
92
+ 0
93
+ end
94
+ end
95
+
96
+ # @rbs (Layer layer, String bytes, Integer pos, Integer ending) -> Hash[String, Array[String]]
97
+ def parse_headers(layer, bytes, pos, ending)
98
+ # @type var headers: Hash[String, Array[String]]
99
+ headers = {}
100
+ while pos < ending - 2
101
+ after = bytes.index("\r\n", pos) #: Integer
102
+ line = bytes.byteslice(pos, after - pos) #: String
103
+ colon = line.index(':')
104
+ name = colon ? line.byteslice(0, colon) : '' #: String
105
+ unless colon && name.match?(TOKEN)
106
+ layer.diagnose(:error, :malformed, 'invalid HTTP header name')
107
+ return headers
108
+ end
109
+ name = name.downcase
110
+ value = line.byteslice(colon + 1..) #: String
111
+ leading = (value[/\A[ \t]*/n] || '').bytesize
112
+ trailing = value.rindex(/[^ \t]/n)
113
+ value = value.byteslice(leading, trailing ? trailing + 1 - leading : 0) #: String
114
+ (headers[name] ||= []) << value
115
+ key = name.tr('-', '_')
116
+ if HEADERS.include?(key)
117
+ numeric = key == 'content_length' && value.match?(/\A[0-9]+\z/n)
118
+ put(layer, "http.#{key}", numeric ? value.to_i : value, numeric ? :uint : :string,
119
+ pos + colon + 1 + leading, value.bytesize)
120
+ else
121
+ put(layer, 'http.request.line', line, :string, pos, line.bytesize)
122
+ end
123
+ layer.diagnose(:error, :malformed, 'control byte in HTTP header') if value.match?(/[\x00-\x08\x0a-\x1f\x7f]/n)
124
+ pos = after + 2
125
+ end
126
+ headers
127
+ end
128
+
129
+ # @rbs (Layer layer, String bytes, Integer pos) -> [String, Integer]
130
+ def chunked(layer, bytes, pos)
131
+ body = ''.b
132
+ loop do
133
+ ending = bytes.index("\r\n", pos)
134
+ unless ending
135
+ layer.diagnose(:note, :truncated, 'incomplete HTTP chunk size')
136
+ return [body, bytes.bytesize]
137
+ end
138
+ line = bytes.byteslice(pos, ending - pos) #: String
139
+ size = line.split(';', 2).first || ''
140
+ unless size.match?(/\A[0-9a-fA-F]{1,16}\z/n)
141
+ layer.diagnose(:error, :malformed, 'invalid HTTP chunk size')
142
+ return [body, ending + 2]
143
+ end
144
+ length = size.to_i(16)
145
+ pos = ending + 2
146
+ if length.zero?
147
+ loop do
148
+ ending = bytes.index("\r\n", pos)
149
+ unless ending
150
+ layer.diagnose(:note, :truncated, 'incomplete HTTP trailers')
151
+ return [body, bytes.bytesize]
152
+ end
153
+ return [body, ending + 2] if ending == pos
154
+ put(layer, 'http.chunked.trailer', bytes.byteslice(pos, ending - pos), :string, pos, ending - pos)
155
+ pos = ending + 2
156
+ end
157
+ end
158
+ available = [length, bytes.bytesize - pos].min
159
+ fragment = bytes.byteslice(pos, available) #: String
160
+ body << fragment
161
+ if available < length || pos + length + 2 > bytes.bytesize
162
+ layer.diagnose(:note, :truncated, 'incomplete HTTP chunk')
163
+ return [body, bytes.bytesize]
164
+ end
165
+ unless bytes.byteslice(pos + length, 2) == "\r\n"
166
+ layer.diagnose(:error, :malformed, 'HTTP chunk lacks CRLF')
167
+ return [body, pos + length]
168
+ end
169
+ pos += length + 2
170
+ end
171
+ end
172
+
173
+ # @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length) -> void
174
+ def put(layer, name, value, type, offset, length)
175
+ layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
176
+ end
177
+
178
+ # @rbs (Layer layer) -> String
179
+ def summary(layer)
180
+ names = layer.field_value('http.request.method') ? %w[http.request.method http.request.uri] : %w[http.response.code http.response.phrase]
181
+ 'HTTP ' + names.filter_map { |name| layer.fields.find { |field| field.name == name }&.display }.join(' ')
182
+ end
183
+ end
184
+ end
185
+ end