Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 497 lines (394 sloc) 19.225 kb
b567b74 @mranney Add simple README
authored
1 node_pcap
2 =========
3
58701b8 @mranney Add some actual documentation and some updated examples.
authored
4 This is a set of bindings from `libpcap` to node as well as some useful libraries to decode, print, and
5 analyze packets. `libpcap` is a packet capture library used by programs like `tcpdump` and `wireshark`.
6 It has been tested on OSX and Linux.
b567b74 @mranney Add simple README
authored
7
58701b8 @mranney Add some actual documentation and some updated examples.
authored
8 Sadly, `node_pcap` is _not done_ yet. While it is incomplete, it is still already useful for capturing
0ebc243 @mranney Improve error messages and documentation.
authored
9 and manipulating packets in JavaScript. The best example of this so far is `http_trace`, described below.
58701b8 @mranney Add some actual documentation and some updated examples.
authored
10
11 ## Why?
12
13 There are already many tools for capturing, decoding, and analyzing packets. Many of them are thoroughly
14 tested and very fast. Why would anybody want to capture and manipulate packets in JavaScript? A few reasons:
15
16 * JavaScript makes writing event-based programs very natural. Each packet that is captured generates an
17 event, and as higher level protocols are decoded, they might generate events as well. Writing code to handle
18 these events is much easier and more readable with anonymous functions and closures.
19
20 * node makes handling binary data in JavaScript fast and efficient with its Buffer class. Decoding packets involves
21 a lot of binary slicing and dicing which can be awkward with JavaScript strings.
22
23 * Writing servers that capture packets, process them somehow, and then serve the processed data up in some way is
24 very straightforward in node.
25
0ebc243 @mranney Improve error messages and documentation.
authored
26 * Node has a very good HTTP parser that is used to progressively decode HTTP sessions.
b567b74 @mranney Add simple README
authored
27
3054c0e @mranney Add installation example
authored
28 ## Installation
29
30 You will need `libpcap` installed. Most OSX machines seem to have it. All major Linux distributions have it available
31 either by default or with a package like `libpcap-dev`.
32
878080a @mranney Update installation instructions.
authored
33 The easiest way to get `node_pcap` and its tools is with `npm`:
34
35 npm install pcap
36
37 If you want to hack on the source code, you can get it from github. Clone the repo like this:
3054c0e @mranney Add installation example
authored
38
39 git clone git://github.com/mranney/node_pcap.git
40
41 To compile the native code bindings, do this:
42
43 cd node_pcap
44 node-waf configure build
45
46 Assuming it built without errors, you should be able to run the examples and then write your own packet
47 capture programs.
48
b567b74 @mranney Add simple README
authored
49
06423e3 @mranney Update examples.
authored
50 ## Usage
b567b74 @mranney Add simple README
authored
51
58701b8 @mranney Add some actual documentation and some updated examples.
authored
52 There are several example programs that show how to use `node_pcap`. These examples are best documentation.
53 Try them out and see what they do.
54
35011ae @mranney Documentation updates for TCP_tracker events
authored
55 To use this library in your own program, `pcap.js` and `pcap_binding.node` must be in `NODE_PATH`. `npm`
0ebc243 @mranney Improve error messages and documentation.
authored
56 takes care of this automatically.
57
35011ae @mranney Documentation updates for TCP_tracker events
authored
58 ### Starting a capture session
59
60 To start a capture session, call `pcap.createSession` with an interface name and a pcap filter string:
58701b8 @mranney Add some actual documentation and some updated examples.
authored
61
0ebc243 @mranney Improve error messages and documentation.
authored
62 var pcap = require('pcap'),
58701b8 @mranney Add some actual documentation and some updated examples.
authored
63 pcap_session = pcap.createSession(interface, filter);
64
65 `interface` is the name of the interface on which to capture packets. If passed an empty string, `libpcap`
0ebc243 @mranney Improve error messages and documentation.
authored
66 will try to pick a "default" interface, which is often just the first one in some list and not what you want.
58701b8 @mranney Add some actual documentation and some updated examples.
authored
67
68 `fitler` is a pcap filter expression, see `pcap-filter(7)` for more information. An empty string will capture
69 all packets visible on the interface.
06423e3 @mranney Update examples.
authored
70
35011ae @mranney Documentation updates for TCP_tracker events
authored
71 Note that `node_pcap` always opens the interface in promiscuous mode, which generally requires running as root.
72 Unless you are recklessly roaming about as root already, you'll probably want to start your node program like this:
73
74 sudo node test.js
75
58701b8 @mranney Add some actual documentation and some updated examples.
authored
76 `pcap_session` is an `EventEmitter` that emits a `packet` event. The only argument to the callback will be a
77 `Buffer` object with the raw bytes returned by `libpcap`.
78
79 Listening for packets:
80
35011ae @mranney Documentation updates for TCP_tracker events
authored
81 pcap_session.on('packet', function (raw_packet) {
58701b8 @mranney Add some actual documentation and some updated examples.
authored
82 // do some stuff with a raw packet
06423e3 @mranney Update examples.
authored
83 });
b567b74 @mranney Add simple README
authored
84
35011ae @mranney Documentation updates for TCP_tracker events
authored
85 To convert `raw_packet` into a JavaScript object that is easy to work with, decode it:
58701b8 @mranney Add some actual documentation and some updated examples.
authored
86
87 var packet = pcap.decode.packet(raw_packet);
88
89 The protocol stack is exposed as a nested set of objects. For example, the TCP destination port is part of TCP
90 which is encapsulated within IP, which is encapsulated within a link layer. Access it like this:
91
92 packet.link.ip.tcp.dport
ad9d634 @mranney Update examples and show payload dump.
authored
93
58701b8 @mranney Add some actual documentation and some updated examples.
authored
94 This structure is easy to explore with `sys.inspect`.
ad9d634 @mranney Update examples and show payload dump.
authored
95
35011ae @mranney Documentation updates for TCP_tracker events
authored
96 ### TCP Analysis
97
98 TCP can be analyzed by feeding the packets into a `TCP_tracker` and then listening for `start` and `end` events.
99
100 var pcap = require('pcap'),
101 tcp_tracker = new pcap.TCP_tracker(),
102 pcap_session = pcap.createSession(interface, "ip proto \tcp");
103
104 tcp_tracker.on('start', function (session) {
105 console.log("Start of TCP session between " + session.src_name + " and " + session.dst_name);
106 });
107
108 tcp_tracker.on('end', function (session) {
109 console.log("End of TCP session between " + session.src_name + " and " + session.dst_name);
110 });
111
112 pcap_session.on('packet', function (raw_packet) {
113 var packet = pcap.decode.packet(raw_packet);
114 tcp_tracker.track_packet(packet);
115 });
116
117 You must only send IPv4 TCP packets to the TCP tracker. Explore the `session` object with `sys.inspect` to
118 see the wonderful things it can do for you. Hopefully the names of the properties are self-explanatory:
119
120 { src: '10.51.2.130:55965'
121 , dst: '75.119.207.0:80'
122 , syn_time: 1280425738896.771
123 , state: 'ESTAB'
124 , key: '10.51.2.130:55965-75.119.207.0:80'
125 , send_isn: 2869922608
126 , send_window_scale: 8
127 , send_packets: { '2869922609': 1280425738896.771 }
128 , send_acks: { '1063203923': 1280425738911.618 }
129 , send_retrans: {}
130 , send_next_seq: 2869922609
131 , send_acked_seq: null
132 , send_bytes_ip: 60
133 , send_bytes_tcp: 108
134 , send_bytes_payload: 144
135 , recv_isn: 1063203922
136 , recv_window_scale: 128
137 , recv_packets: { '1063203923': 1280425738911.536 }
138 , recv_acks: { '2869922609': 1280425738911.536 }
139 , recv_retrans: {}
140 , recv_next_seq: null
141 , recv_acked_seq: null
142 , recv_bytes_ip: 20
143 , recv_bytes_tcp: 40
144 , recv_bytes_payload: 0
145 , src_name: '10.51.2.130:55965'
146 , dst_name: '75.119.207.0:80'
147 , current_cap_time: 1280425738911.65
148
149 ### HTTP Analysis
150
151 The `TCP_tracker` also detects and decodes HTTP on all streams it receives. If HTTP is detected, several
7f16837 @mranney Documentation updates for HTTP events.
authored
152 new events will be emitted:
153
154 * `http_request`: function(session, http)
155 * `http_request_body`: function(session, http, data)
156
157 Note that `data` is a node Buffer object sliced from the original packet. If you want to use it past the
158 current tick, you'll need to make a copy somehow.
159
160 * `http_request_complete`: function(session, http)
161 * `http_response`: function(session, http)
162 * `http_response_body`: function(session, http, data)
163
164 `data` is a Buffer slice. See above.
165
166 * `http_response_complete`: function(session, http)
35011ae @mranney Documentation updates for TCP_tracker events
authored
167
168 See `examples/http_trace` for an example of how to use these events to decode HTTP.
169
170 ### WebSocket Analysis
171
172 The `TCP_tracker` further detects and decodes WebSocket traffic on all streams it receives.
173
174 * `websocket_upgrade`: function(session, http)
175 * `websocket_message`: function(session, dir, message)
176
177 See `examples/http_trace` for an example of how to use these events to decode WebSocket.
ad9d634 @mranney Update examples and show payload dump.
authored
178
0ebc243 @mranney Improve error messages and documentation.
authored
179
180 ## Some Common Problems
181
182 ### TCP Segmentation Offload - TSO
183
184 TSO is a technique that modern operating systems use to offload the burden of IP/TCP header computation to
185 the network hardware. It also reduces the number of times that data is moved data between the kernel and the
186 network hardware. TSO saves CPU when sending data that is larger than a single IP packet.
187
188 This is amazing and wonderful, but it does make some kinds of packet sniffing more difficult. In many cases,
189 it is important to see the exact packets that are sent, but if the network hardware is sending the packets,
190 these are not available to `libpcap`. The solution is to disable TSO.
191
192 OSX:
193
194 sudo sysctl -w net.inet.tcp.tso=0
195
196 Linux (substitute correct interface name):
197
198 sudo ethtool -K eth0 tso off
199
200 The symptoms of needing to disable TSO are messages like, "Received ACK for packet we didn't see get sent".
201
202 ### IPv6
203
204 Sadly, `node_pcap` does not know how to decode IPv6 packets yet. Often when capturing traffic to `localhost`, IPv6 traffic
205 will arrive surprisingly, even though you were expecting IPv4. A common case is the hostname `localhost`, which many client programs will
206 resolve to the IPv6 address `::1` and then will try `127.0.0.1`. Until we get IPv6 decode support, a `libpcap` filter can be
207 set to only see IPv4 traffic:
208
35011ae @mranney Documentation updates for TCP_tracker events
authored
209 sudo http_trace lo0 "ip proto \tcp"
0ebc243 @mranney Improve error messages and documentation.
authored
210
211 The backslash is important. The pcap filter language has an ambiguity with the word "tcp", so by escaping it,
212 you'll get the correct interpretation for this case.
213
214 ### Dropped packets
215
216 There are several levels of buffering involved in capturing packets. Sometimes these buffers fill up, and
217 you'll drop packets. If this happens, it becomes difficult to reconstruct higher level protocols. The best
218 way to keep the buffers from filling up is to use pcap filters to only consider traffic that you need to decode.
219 The pcap filters are very efficient and run close to the kernel where they can process high packet rates.
220
221 If the pcap filters are set correctly and `libpcap` still drops packets, it is possible to increase `libpcap`'s
222 buffer size. At the moment, this requires changing `pcap_binding.cc`. Look for `pcap_set_buffer_size()` and
223 set to a larger value.
3663bd0 @mranney Add http_trace.js example.
authored
224
46b1ea7 @mranney Update docs for http_trace slightly and add image.
authored
225 ## examples/http_trace
226
6f43642 @mranney Support for WebSocket decoding and printing
authored
227 This is a handy standalone program that can help diagnose HTTP and WebSocket traffic.
3663bd0 @mranney Add http_trace.js example.
authored
228
229 The TCP tracker looks for HTTP at the beginning of every TCP connection. If found, all captured on this connection
46b1ea7 @mranney Update docs for http_trace slightly and add image.
authored
230 will be fed to node's HTTP parser and events will be generated. `http_trace` has listeners for these events and will
231 print out some helpful information.
232
6f43642 @mranney Support for WebSocket decoding and printing
authored
233 If a WebSocket upgrade is detected, `http_trace` will start looking for WebSocket messages on that connection.
234
a86e1fa @mranney Fix image URL.
authored
235 ![http_trace screenshot](http://ranney.com/httptrace.jpg)
46b1ea7 @mranney Update docs for http_trace slightly and add image.
authored
236
237
238 ## examples/simple_capture
58701b8 @mranney Add some actual documentation and some updated examples.
authored
239
35011ae @mranney Documentation updates for TCP_tracker events
authored
240 This program captures packets and prints them using the built in simple printer. Here's a sample of it's output.
58701b8 @mranney Add some actual documentation and some updated examples.
authored
241 In another window I ran `curl nodejs.org`.
242
243 mjr:~/work/node_pcap$ sudo node examples/simple_capture.js en1 ""
244 libpcap version 1.0.0
245 en0 no address
246 * en1 10.240.0.133/255.255.255.0
247 lo0 127.0.0.1/255.0.0.0
248 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 10.240.0.1 ARP request 10.240.0.133
249 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c 10.240.0.133 ARP reply 10.240.0.1 hwaddr 00:18:39:ff:f9:1c
250 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c 10.240.0.133:53808 -> 97.107.132.72:80 TCP len 64
251 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c 10.240.0.133:57052 -> 10.240.0.1:53 DNS question 133.0.240.10.in-addr.arpa PTR
252 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c 10.240.0.133:57052 -> 10.240.0.1:53 DNS question 72.132.107.97.in-addr.arpa PTR
253 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c 10.240.0.133:57052 -> 10.240.0.1:53 DNS question 1.0.240.10.in-addr.arpa PTR
254 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 10.240.0.1:53 -> 10.240.0.133:57052 DNS answer 133.0.240.10.in-addr.arpa PTR
255 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 10.240.0.1:53 -> rv-mjr2.ranney.com:57052 DNS answer 72.132.107.97.in-addr.arpa PTR
256 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 10.240.0.1:53 -> rv-mjr2.ranney.com:57052 DNS answer 1.0.240.10.in-addr.arpa PTR
257 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 60
258 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
259 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 196
260 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 52
261 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
262 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
263 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
264 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
265 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
266 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
267 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
268 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
269 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
270 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
271 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 1500
272 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 337
273 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
274 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
275 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
276 00:18:39:ff:f9:1c -> 00:1f:5b:ce:3e:29 tinyclouds.org:80 -> rv-mjr2.ranney.com:53808 TCP len 52
277 00:1f:5b:ce:3e:29 -> 00:18:39:ff:f9:1c rv-mjr2.ranney.com:53808 -> tinyclouds.org:80 TCP len 52
ad9d634 @mranney Update examples and show payload dump.
authored
278
279
280 ## Output from `session.findalldevs`:
b567b74 @mranney Add simple README
authored
281
282 [ { name: 'en0'
283 , addresses:
284 [ { addr: '10.51.2.183'
285 , netmask: '255.255.255.0'
286 , broadaddr: '10.51.2.255'
287 }
288 ]
289 }
290 , { name: 'fw0', addresses: [] }
291 , { name: 'en1', addresses: [] }
292 , { name: 'lo0'
293 , addresses: [ { addr: '127.0.0.1', netmask: '255.0.0.0' } ]
294 , flags: 'PCAP_IF_LOOPBACK'
295 }
296 ]
297
298
58701b8 @mranney Add some actual documentation and some updated examples.
authored
299 ### Deep decode of `curl nodejs.org`:
300
301 Running `sys.inspect` on the first three decoded packets of this TCP session.
b9a7d4a @mranney Update example output.
authored
302
303 First packet, TCP SYN:
304
305 { ethernet:
306 { dhost: '00:18:39:ff:f9:1c'
307 , shost: '00:1f:5b:ce:3e:29'
308 , ethertype: 2048
309 , ip:
310 { version: 4
311 , header_length: 5
312 , diffserv: 0
313 , total_length: 64
314 , identification: 49042
315 , flags: { reserved: 0, df: 1, mf: 0 }
316 , fragment_offset: 0
317 , ttl: 64
318 , protocol: 6
319 , header_checksum: 35325
320 , saddr: '10.240.0.133'
321 , daddr: '97.107.132.72'
322 , protocol_name: 'TCP'
323 , tcp:
324 { sport: 57230
325 , dport: 80
326 , seqno: 4179361823
327 , ackno: 1540242985
328 , data_offset: 11
329 , reserved: 0
330 , flags:
331 { cwr: 0
332 , ece: 0
333 , urg: 0
334 , ack: 0
335 , psh: 0
336 , rst: 0
337 , syn: 1
338 , fin: 0
339 }
340 , window_size: 65535
341 , checksum: 2601
342 , urgent_pointer: 0
343 , payload_offset: 78
344 , payload: { length: 0 }
345 }
346 }
347 }
348 , pcap_header:
349 { time: Sat, 22 May 2010 07:48:40 GMT
350 , tv_sec: 1274514520
351 , tv_usec: 820479
352 , caplen: 78
353 , len: 78
354 , link_type: 'LINKTYPE_ETHERNET'
355 }
b567b74 @mranney Add simple README
authored
356 }
b9a7d4a @mranney Update example output.
authored
357
358 Second packet, TCP SYN+ACK:
359
360 { ethernet:
361 { dhost: '00:1f:5b:ce:3e:29'
362 , shost: '00:18:39:ff:f9:1c'
363 , ethertype: 2048
364 , ip:
365 { version: 4
366 , header_length: 5
367 , diffserv: 32
368 , total_length: 60
369 , identification: 0
370 , flags: { reserved: 0, df: 1, mf: 0 }
371 , fragment_offset: 0
372 , ttl: 48
373 , protocol: 6
374 , header_checksum: 22900
375 , saddr: '97.107.132.72'
376 , daddr: '10.240.0.133'
377 , protocol_name: 'TCP'
378 , tcp:
379 { sport: 80
380 , dport: 57230
381 , seqno: 1042874392
382 , ackno: 973076764
383 , data_offset: 10
384 , reserved: 0
385 , flags:
386 { cwr: 0
387 , ece: 0
388 , urg: 0
389 , ack: 1
390 , psh: 0
391 , rst: 0
392 , syn: 1
393 , fin: 0
394 }
395 , window_size: 5792
396 , checksum: 35930
397 , urgent_pointer: 0
398 , payload_offset: 74
399 , payload: { length: 0 }
400 }
401 }
402 }
403 , pcap_header:
404 { time: Sat, 22 May 2010 07:48:40 GMT
405 , tv_sec: 1274514520
406 , tv_usec: 915980
407 , caplen: 74
408 , len: 74
409 , link_type: 'LINKTYPE_ETHERNET'
410 }
06423e3 @mranney Update examples.
authored
411 }
b9a7d4a @mranney Update example output.
authored
412
413 Third packet, TCP ACK, 3-way handshake is now complete:
414
415 { ethernet:
416 { dhost: '00:18:39:ff:f9:1c'
417 , shost: '00:1f:5b:ce:3e:29'
418 , ethertype: 2048
419 , ip:
420 { version: 4
421 , header_length: 5
422 , diffserv: 0
423 , total_length: 52
424 , identification: 39874
425 , flags: { reserved: 0, df: 1, mf: 0 }
426 , fragment_offset: 0
427 , ttl: 64
428 , protocol: 6
429 , header_checksum: 44505
430 , saddr: '10.240.0.133'
431 , daddr: '97.107.132.72'
432 , protocol_name: 'TCP'
433 , tcp:
434 { sport: 57230
435 , dport: 80
436 , seqno: 4179361823
437 , ackno: 1540242985
438 , data_offset: 8
439 , reserved: 0
440 , flags:
441 { cwr: 0
442 , ece: 0
443 , urg: 0
444 , ack: 1
445 , psh: 0
446 , rst: 0
447 , syn: 0
448 , fin: 0
449 }
450 , window_size: 65535
451 , checksum: 53698
452 , urgent_pointer: 0
453 , payload_offset: 66
454 , payload: { length: 0 }
455 }
456 }
457 }
458 , pcap_header:
459 { time: Sat, 22 May 2010 07:48:40 GMT
460 , tv_sec: 1274514520
461 , tv_usec: 916054
462 , caplen: 66
463 , len: 66
464 , link_type: 'LINKTYPE_ETHERNET'
465 }
466 }
467
0ebc243 @mranney Improve error messages and documentation.
authored
468 ## Help Wanted
469
7f16837 @mranney Documentation updates for HTTP events.
authored
470 I want to build up decoders and printers for all popular protocols. Patches are welcome.
0ebc243 @mranney Improve error messages and documentation.
authored
471
472
31a0616 @mranney Decode ARP.
authored
473 ## LICENSE - "MIT License"
474
475 Copyright (c) 2010 Matthew Ranney, http://ranney.com/
476
477 Permission is hereby granted, free of charge, to any person
478 obtaining a copy of this software and associated documentation
479 files (the "Software"), to deal in the Software without
480 restriction, including without limitation the rights to use,
481 copy, modify, merge, publish, distribute, sublicense, and/or sell
482 copies of the Software, and to permit persons to whom the
483 Software is furnished to do so, subject to the following
484 conditions:
485
486 The above copyright notice and this permission notice shall be
487 included in all copies or substantial portions of the Software.
488
489 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
490 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
491 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
492 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
493 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
494 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
495 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
496 OTHER DEALINGS IN THE SOFTWARE.
Something went wrong with that request. Please try again.