web_server_uid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA512:
3
+ metadata.gz: 1f4e8a55bfb690af19adc6020144a1239ce1958253d82cde98a5863735535889a451e116f12dcfd33927b3bbe8541930cc5260be1bf1c8f8021cccc09ad0b4f7
4
+ data.tar.gz: a5b54a5ea1c6a90cca40e472fc7909b3ae23298f6d13cd4ddf54ed23940ad85c125c659d3465e30987aefce6e0e3446e01f6e020dcda4b9cf92ce1bb85c98697
5
+ SHA1:
6
+ metadata.gz: 29eb5fec9ff2853494d6752ae159f5df63a40977
7
+ data.tar.gz: 7730d4f45312472340a561d486bdb960eb1a094e
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - "2.1.0"
3
+ - "2.0.0"
4
+ - "1.9.3"
5
+ - "1.8.7"
6
+ - "jruby-1.7.9"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in web_server_uid.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Swiftype, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # WebServerUid
2
+
3
+ WebServerUid is a small gem that can be used to represent "UIDs" in your application, where a "UID" is a unique ID
4
+ generated by Apache's [`mod_uid`](http://www.lexa.ru/programs/mod-uid-eng.html) or nginx's
5
+ [`http_userid_module`](http://nginx.org/en/docs/http/ngx_http_userid_module.html). Using these modules, you can
6
+ generate a unique ID for each visitor to your website, before they log in, and add it to all logged data, from database
7
+ rows to application logs to web-server logs to anything else you may desire.
8
+
9
+ Generating a unique ID for each visitor is not terribly difficult and can easily be done within (_e.g._) Rails, by
10
+ simply creating a large unique value (like a UUID) and assigning it to a cookie. However, there is one huge caveat to
11
+ this approach: the very first request the visitor makes to your site will _not_ be tagged in your web-server logs
12
+ with this unique ID, because they will not have had the cookie present in their browser when they made the request.
13
+ This initial request is critical, because it contains the HTTP referer (_i.e._, how the user _got_ to your site in the
14
+ first place) and the landing page (what first page were they directed to?), and this is extremely valuable
15
+ information.
16
+
17
+ By using `mod_uid` or `http_userid_module`, the web server itself can generate the user ID and log it even on this
18
+ very first request. For example, we can easily configure `nginx` to do this with something like:
19
+
20
+ userid on;
21
+ userid_name brid;
22
+ userid_domain 'foo.com';
23
+ userid_path /;
24
+ userid_expires max;
25
+ userid_mark S;
26
+
27
+ You can then pass it to your Rails application by doing something like this (in nginx, for example):
28
+
29
+ proxy_set_header X-Nginx-Browser-ID-Got $uid_got;
30
+ proxy_set_header X-Nginx-Browser-ID-Set $uid_set;
31
+
32
+ ...and then you'll get data like the following:
33
+
34
+ request.env['HTTP_X_NGINX_BROWSER_ID_GOT'] # => brid=0100007FE7D7F35241946D1E02030303
35
+
36
+ or
37
+
38
+ request.env['HTTP_X_NGINX_BROWSER_ID_SET'] # => brid=0100007FE7D7F35241946D1E02030303
39
+
40
+ Specifically, you'll get `HTTP_X_NGINX_BROWSER_ID_SET` (but not `_GOT`) on the very first request, and then
41
+ `HTTP_X_NGINX_BROWSER_ID_GOT` on each subsequent request. You'll _also_ get a cookie — `cookies[:brid]` —
42
+ that will look like `fwAAAVLz1+cebZRBAwMDAgS=`; this is a Base64-encoded, endianness-reversed version of the exact
43
+ same data.
44
+
45
+ WebServerUid can help you easily parse the data above, return either form from any input, return a
46
+ pure-binary version of this data (which is only 16 bytes long — the shortest possible form if you want to store
47
+ the data in a database or in log files). It also compares correctly (meaning `<`, `>=`, `==`, `!=`, `eql?`, and so on
48
+ work properly), and hashes correctly (meaning you can store it in a Hash, and find it again, even if searching via a
49
+ different actual object that is in fact equal to the original key).
50
+
51
+ Because these UIDs also have internal structure (including the IP address of the server that generated them and the
52
+ time at which they were generated, among other), WebServerUid also has methods that will return this data for you.
53
+
54
+ **NOTE**: You'll probably be happier if you actually term this a _browser ID_ in your application, because that's what
55
+ it actually is (the implied "user" from `uid` notwithstanding); these cookies get set uniquely per browser, and never
56
+ cleared (unless you manually clear them &mdash; and then the web server will just re-set them, anyway). Having a
57
+ per-browser unique ID is an incredibly valuable thing for your analytics, and nicely orthogonal to your concept of
58
+ user &mdash; it's just important that you keep the distinction clear in your mind.
59
+
60
+ WebServerUid supports:
61
+
62
+ * Ruby 1.8.7, 1.9.3, 2.0.0, 2.1.0, or JRuby 1.7.9
63
+
64
+ These are, however, just the versions it's tested against; WebServerUid contains no code that should be at all
65
+ particularly dependent on exact Ruby versions, and should be compatible with a broad set of versions.
66
+
67
+ Current build status: ![Current Build Status](https://api.travis-ci.org/swiftype/web_server_uid.png?branch=master)
68
+
69
+ ## Installation
70
+
71
+ Add this line to your application's Gemfile:
72
+
73
+ gem 'web_server_uid'
74
+
75
+ And then execute:
76
+
77
+ $ bundle
78
+
79
+ Or install it yourself as:
80
+
81
+ $ gem install web_server_uid
82
+
83
+ ## Usage
84
+
85
+ For the most common case &mdash; you can parse a WebServerUid out of one of those `request.env` lines by doing:
86
+
87
+ uid = WebServerUid.from_header(request.env['HTTP_X_NGINX_BROWSER_ID_GOT'], 'brid')
88
+
89
+ You can parse it from a cookie by doing:
90
+
91
+ uid = WebServerUid.from_base64(cookies[:brid])
92
+
93
+ Generally, you want to try all three sources; you can define a method like this, in your `ApplicationController`
94
+ (but make sure the headers and cookie names are correct for your purposes):
95
+
96
+ def web_server_uid
97
+ [ WebServerUid.from_header(request.env['HTTP_X_NGINX_BROWSER_ID_SET'], 'brid'),
98
+ WebServerUid.from_header(request.env['HTTP_X_NGINX_BROWSER_ID_GOT'], 'brid'),
99
+ WebServerUid.from_base64(cookies[:brid]) ].compact.first
100
+ end
101
+
102
+ (These methods properly just return `nil`, rather than failing, if passed `nil`, so this is safe to do.) This will
103
+ return the first value that's set from the set. (While, theoretically, this will never return `nil`, you don't want to
104
+ rely on this, based on experience; automated ping checks, misconfigured front-end servers, and so forth mean that at
105
+ some point you'll almost certainly get a request that somehow has none of the above set.)
106
+
107
+ These methods will raise `ArgumentError` if the data passed in is incorrectly-formatted; it's up to you to decide
108
+ whether you want to allow this to propagate, or swallow it and proceed without a UID.
109
+
110
+ Once you have a WebServerUid, you can call these methods on it:
111
+
112
+ web_server_uid.to_hex_string # => "0100007FE7D7F35241946D1E02030303"
113
+ web_server_uid.to_base64_string # => "fwAAAVLz1+cebZRBAwMDAg=="
114
+ web_server_uid.to_binary_string # => "\177\000\000\001R\363\327\347\036m\224A\003\003\003\002"
115
+
116
+ This should give you pretty much any data you could want to work with these values. Note that there are also class
117
+ methods `from_binary` and `from_base64` that will accept strings of those formats, too.
118
+
119
+ All the ordinary comparison operators &mdash; `<`, `<=`, `>`, `>=`, `<=>`, `==`, `!=`, and `eql?` &mdash; work
120
+ properly on values of this class; it also hashes properly, so feel free to use it as a hash key.
121
+
122
+ ## Contributing
123
+
124
+ 1. Fork it ( http://github.com/swiftype/web_server_uid/fork )
125
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
126
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
127
+ 4. Push to the branch (`git push origin my-new-feature`)
128
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,3 @@
1
+ class WebServerUid
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,297 @@
1
+ require "base64"
2
+ require "ipaddr"
3
+ require "web_server_uid/version"
4
+
5
+ # A WebServerUid represents a UID token, as issued by web browsers like Apache
6
+ # (mod_uid, http://www.lexa.ru/programs/mod-uid-eng.html) or nginx (http_userid_module,
7
+ # http://nginx.org/en/docs/http/ngx_http_userid_module.html).
8
+ #
9
+ # (Note that while this is called a "UID", it is almost certainly better understood as a "browser ID", because it is
10
+ # unique to each browser and very unlikely to be managed in the same way as any "current user" concept you have.)
11
+ #
12
+ # UID tokens can be very useful when tracking visitors to your site, and more so than just setting a unique cookie
13
+ # from your Rails app, for exactly one reason: since your front-end web server can issue and set the cookie directly,
14
+ # it means that you can get the UID logged on the very first request visitors make to your site -- which is often a
15
+ # really critical one, since it tells you how they got there in the first place (the HTTP referer) and which page
16
+ # they first viewed (the landing page).
17
+ #
18
+ # So, generally, you'll want to do this:
19
+ #
20
+ # * Turn on +mod_uid+ or +http_userid_module+.
21
+ # * Add the UID to the logs -- in nginx, you'll want to log _both_ +$uid_got+ and +$uid_set+, to handle both the case
22
+ # where you've already seen the browser before and the case where you haven't.
23
+ # * In your Rails application,
24
+ class WebServerUid
25
+ # This contains all Base64 characters from all possible variants of Base64, according to
26
+ # http://en.wikipedia.org/wiki/Base64 -- this is so that we accept Base64-encoded UID cookies,
27
+ # no matter what their source.
28
+ BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/-_\\.:!"
29
+ # This is, similarly, all characters that can be used as Base64 padding
30
+ BASE64_PADDING = "=-"
31
+ # This is a Regexp that matches any valid Base64 data
32
+ BASE64_REGEX = Regexp.new("^[#{BASE64_ALPHABET}]+[#{BASE64_PADDING}]*$")
33
+
34
+ # How long is the raw binary data required to be (in bytes) after we decode it?
35
+ RAW_BINARY_LENGTH = 16
36
+ # By default, how much extra binary data (in bytes) should we allow?
37
+ DEFAULT_ALLOWED_EXTRA_BINARY_DATA = 1
38
+
39
+ class << self
40
+ # Creates a new instance from a hex string; see #initialize for more details. Nicely returns nil if passed nil.
41
+ def from_hex(h, options = { })
42
+ new(h, :hex, options) if h
43
+ end
44
+
45
+ # Creates a new instance from a binary string; see #initialize for more details. Nicely returns nil if passed nil.
46
+ def from_binary(b, options = { })
47
+ new(b, :binary, options) if b
48
+ end
49
+
50
+ # Creates a new instance from a base64 string; see #initialize for more details. Nicely returns nil if passed nil.
51
+ def from_base64(b, options = { })
52
+ new(b, :base64, options) if b
53
+ end
54
+
55
+ # Given a string like "st_brid=0100007FE7D7F35241946D1E02030303", and the expected name of the ID cookie
56
+ # (_e.g._, +st_brid+), returns a WebServerUid if one is found, and nil otherwise. Also returns nil if input is nil.
57
+ # This is the exact format you get in a request.env header if you have lines like these in your nginx config:
58
+ #
59
+ # proxy_set_header X-Nginx-Browser-ID-Got $uid_got;
60
+ # proxy_set_header X-Nginx-Browser-ID-Set $uid_set;
61
+ #
62
+ # This is just a simple little method to make your parsing a bit easier.
63
+ def from_header(s, expected_name)
64
+ if s && s =~ /#{expected_name}\s*\=\s*([0-9A-F]{32})/i
65
+ from_hex($1)
66
+ end
67
+ end
68
+
69
+ # Generates a brand-new instance, from scratch. This follows exactly the algorithm in nginx-1.5.10:
70
+ #
71
+ # * The first four bytes are the local IP address (entire if IPv4, four LSB if IPv6);
72
+ # * The next four bytes are the current time, as a Unix epoch time;
73
+ # * The next two bytes are a function of the start time of the process, but LSBs in microseconds;
74
+ # * The next two bytes are the PID of the process;
75
+ # * The next three bytes are a sequence value, starting at 0x030303;
76
+ # * The last byte is 2, for version 2.
77
+ #
78
+ # +options+ can contain:
79
+ #
80
+ # [:ip_address] Must be an IPAddr object to use as the IP address of this machine, in lieu of autodetection
81
+ # (see #find_local_ip_address, below).
82
+ def generate(options = { })
83
+ # Yes, global variables. What what?
84
+ #
85
+ # Well, in certain cases (like under Rails), this class may get unloaded and reloaded. (Yes, it's in a gem, so
86
+ # theoretically this shouldn't happen, but we want to be really, really careful.) Because we need to be really
87
+ # sure to maintain uniqueness, we use global variables, which, unlike class variables, won't get reset if this
88
+ # class gets loaded or unloaded
89
+ $_web_server_uid_start_value ||= ((Time.now.usec / 20) << 16) | (Process.pid & 0xFFFF)
90
+ $_web_server_uid_sequencer ||= 0x030302
91
+ $_web_server_uid_sequencer += 1
92
+ $_web_server_uid_sequencer &= 0xFFFFFF
93
+
94
+ extra = options.keys - [ :ip_address ]
95
+ if extra.length > 0
96
+ raise ArgumentError, "Unknown keys: #{extra.inspect}"
97
+ end
98
+
99
+ ip_address = if options[:ip_address]
100
+ IPAddr.new(options[:ip_address])
101
+ else
102
+ find_local_ip_address
103
+ end
104
+
105
+ components = [
106
+ ip_address.to_i & 0xFFFFFFFF,
107
+ Time.now.to_i,
108
+ $_web_server_uid_start_value,
109
+ ($_web_server_uid_sequencer << 8) | 0x2
110
+ ]
111
+
112
+ binary = components.pack("NNNN")
113
+ from_binary(binary)
114
+ end
115
+
116
+ private
117
+ # Finds the local IP address. This looks like evil voodoo, but it isn't -- no actual network traffic or connection
118
+ # is made. 8.8.8.8 is, famously, one of Google's DNS servers; this tells Ruby to open a UDP socket bound to it --
119
+ # but, unlike TCP, opening a UDP socket doesn't actually do anything until you send something on it. The clever bit
120
+ # is that this will magically find whatever interface your machine would send traffic to Google on, which almost
121
+ # everybody is going to have (even if it's firewalled off somewhere out there on the network), and return the IP
122
+ # address of that.
123
+ #
124
+ # Note that this could be an IPv6 address. This works properly; we grab the four LSB, above.
125
+ #
126
+ # (Much credit to http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/.)
127
+ def find_local_ip_address
128
+ @local_ip_address ||= begin
129
+ require 'socket'
130
+ ipaddr_string = UDPSocket.open {|s| s.connect('8.8.8.8', 1); s.addr.last }
131
+ IPAddr.new(ipaddr_string)
132
+ end
133
+ end
134
+ end
135
+
136
+ # Creates a new WebServerUid object. +raw_data+ must be a String, in one of the following formats:
137
+ #
138
+ # * Hex-encoded -- the format nginx renders them in logs; _e.g._, <tt>0100007FE7D7F35241946D1E02030303</tt>.
139
+ # This is a hex encoding of four *little-endian* four-byte integers underneath.
140
+ # * Base64.encoded -- the format of the actual cookie in client browsers; _e.g._, <tt>fwAAAVLz1+cebZRBAwMDAgS=</tt>.
141
+ # This is a Base64 encoding of four *big-endian* four-byte integers.
142
+ # * Raw binary -- the hex-decoded or Base64-decoded version of above; _e.g._, <tt>\x01\x00\x00\x7F\xE7\xD7\xF3RA\x94m\x1E\x02\x03\x03\x03</tt>.
143
+ # This is expected to be four *big-endian* four-byte integers.
144
+ #
145
+ # ...and +type+ must be the corresponding format -- one of :binary, :hex, or :base64. (It is not possible to guess
146
+ # the format 100% reliably from the inbound +raw_data+, since raw binary can happen to look like one of the others.)
147
+ #
148
+ # +options+ can contain:
149
+ #
150
+ # [:max_allowed_extra_binary_data] If more data is present in the input string than is necessary for the UID to
151
+ # be parsed, this determines how much extra is allowed before an exception is raised;
152
+ # this defaults to 1, since, if you use nginx's +userid_mark+ directive, you'll
153
+ # get exactly that character in the Base64 at the end, and this will translate to
154
+ # extra data.
155
+ def initialize(raw_data, type, options = { })
156
+ raise ArgumentError, "Type must be one of :binary, :hex, or :base64, not #{type.inspect}" unless [ :binary, :hex, :base64 ].include?(type)
157
+ @input_type = type
158
+
159
+ @binary_components = case type
160
+ when :hex then
161
+ @raw_binary_data = [ raw_data ].pack("H*")
162
+ @raw_binary_data.unpack("VVVV")
163
+ when :base64 then
164
+ @raw_binary_data = Base64.decode64(raw_data)
165
+ @raw_binary_data.unpack("NNNN")
166
+ when :binary then
167
+ @raw_binary_data = raw_data
168
+ @raw_binary_data.unpack("NNNN")
169
+ else
170
+ raise "wrong type: #{type.inspect}; need to add support for it?"
171
+ end
172
+
173
+ @extra_binary_data = @raw_binary_data[RAW_BINARY_LENGTH..-1]
174
+ @raw_binary_data = @raw_binary_data[0..(RAW_BINARY_LENGTH - 1)]
175
+
176
+ if @raw_binary_data.length < RAW_BINARY_LENGTH
177
+ raise ArgumentError, "This UID cookie does not appear to be long enough; its raw binary data is of length #{@raw_binary_data.length}, which is less than #{RAW_BINARY_LENGTH.inspect}: #{raw_data.inspect} (became #{@raw_binary_data.inspect})"
178
+ end
179
+
180
+ if @extra_binary_data.length > (options[:max_allowed_extra_binary_data] || DEFAULT_ALLOWED_EXTRA_BINARY_DATA)
181
+ raise ArgumentError, "This UID cookie has #{@extra_binary_data.length} bytes of extra binary data at the end: #{@raw_binary_data.inspect} adds #{@extra_binary_data.inspect}"
182
+ end
183
+ end
184
+
185
+ # This, plus Comparable, implements all the equality and comparison operators we could ever need.
186
+ def <=>(other)
187
+ other_components = other.binary_components
188
+ binary_components.each_with_index do |our_component, index|
189
+ other_component = other_components[index]
190
+ out = our_component <=> other_component
191
+ return out unless out == 0
192
+ end
193
+ 0
194
+ end
195
+
196
+ include Comparable
197
+
198
+ # ...well, except for this one. ;)
199
+ def eql?(other)
200
+ self == other
201
+ end
202
+
203
+ # Let's make sure we hash ourselves correctly, so we, well, work inside a Hash. :)
204
+ def hash
205
+ binary_components.hash
206
+ end
207
+
208
+ # Returns the hex-encoded variant of the UID -- exactly the string that nginx logs to disk or puts in
209
+ # a header created with $uid_got, etc.
210
+ #
211
+ # This will be identical for two equivalent UIDs, no matter what representations they were parsed from.
212
+ def to_hex_string
213
+ @binary_components.pack("VVVV").bytes.map { |b| "%02X" % b }.join("")
214
+ end
215
+
216
+ # Returns the Base64-encoded variant of the UID -- exactly the string that ends up in a cookie in client browsers.
217
+ #
218
+ # This will be identical for two equivalent UIDs, no matter what representations they were parsed from.
219
+ def to_base64_string
220
+ Base64.encode64(@binary_components.pack("NNNN"))
221
+ end
222
+
223
+ # Returns a pure-binary string for this UID.
224
+ #
225
+ # This will be identical for two equivalent UIDs, no matter what representations they were parsed from.
226
+ def to_binary_string
227
+ @binary_components.pack("NNNN")
228
+ end
229
+
230
+ # Returns an Array of length 4; each component will be a single, four-byte Integer, in big-endian byte order,
231
+ # representing the underlying UID.
232
+ def binary_components
233
+ @binary_components
234
+ end
235
+
236
+ # Returns any extra binary data that was supplied (and successfully ignored) past the end of the input string.
237
+ def extra_binary_data
238
+ @extra_binary_data
239
+ end
240
+
241
+ # This is the "service number" -- the first byte of the UID string. Typically, this is the IP address of the
242
+ # server that generated the UID.
243
+ def service_number
244
+ @binary_components[0]
245
+ end
246
+
247
+ # Returns the "service number" as an IPAddr object; you can call #to_s on this to get a string in dotted notation.
248
+ def service_number_as_ip
249
+ IPAddr.new(service_number, Socket::AF_INET)
250
+ end
251
+
252
+ # This is the "issue time" -- the time at which the UID was generated, as a Un*x epoch time -- as an integer.
253
+ def issue_time
254
+ @binary_components[1]
255
+ end
256
+
257
+ # This is the issue time, as a Time object.
258
+ def issue_time_as_time
259
+ Time.at(issue_time)
260
+ end
261
+
262
+ # This is the "process ID" component -- the third four bytes. While this is documented as simply being the process ID
263
+ # of the server process, realistically, servers add more entropy to avoid collisions (and because PIDs are often
264
+ # only two bytes long). Nginx sets the top two bytes to the two least-significant bytes of the current time in
265
+ # microseconds, for example. So we have #pid_component, here, that returns the whole thing, and #pid that returns
266
+ # just the actual PID.
267
+ def pid_component
268
+ @binary_components[2]
269
+ end
270
+
271
+ # As explained above, this is just the PID itself from the third comppnent.
272
+ def pid
273
+ pid_component & 0xFFFF
274
+ end
275
+
276
+ # This is the "sequencer" component -- the last four bytes, which contains both a cookie version number (the LSB)
277
+ # and a sequence number (the three MSBs).
278
+ def sequencer_component
279
+ @binary_components[3]
280
+ end
281
+
282
+ # The actual sequencer value.
283
+ def sequencer
284
+ sequencer_component >> 8
285
+ end
286
+
287
+ # The sequencer value, as a six-byte hex string, which is a much easier way of looking at it (since it's oddly
288
+ # defined to start at 0x030303.)
289
+ def sequencer_as_hex
290
+ "%06x" % sequencer
291
+ end
292
+
293
+ # The version number of the cookie -- the LSB of the sequencer_component.
294
+ def cookie_version_number
295
+ @binary_components[3] & 0xFF
296
+ end
297
+ end
@@ -0,0 +1,309 @@
1
+ require 'web_server_uid'
2
+
3
+ describe WebServerUid do
4
+ describe "class methods" do
5
+ it "should return nil if given nil" do
6
+ expect(WebServerUid.from_hex(nil)).to be_nil
7
+ expect(WebServerUid.from_binary(nil)).to be_nil
8
+ expect(WebServerUid.from_base64(nil)).to be_nil
9
+ end
10
+
11
+ it "should return a value if given one" do
12
+ expect(WebServerUid.from_hex("0100007FE7D7F35241946D1E02030303").to_hex_string).to eq("0100007FE7D7F35241946D1E02030303")
13
+ expect(WebServerUid.from_binary("\177\000\000\001R\363\327\347\036m\224A\003\003\003\002").to_hex_string).to eq("0100007FE7D7F35241946D1E02030303")
14
+ expect(WebServerUid.from_base64("fwAAAVLz1+cebZRBAwMDAgS=").to_hex_string).to eq("0100007FE7D7F35241946D1E02030303")
15
+ end
16
+
17
+ it "should be able to parse a value from a header" do
18
+ expect(WebServerUid.from_header("st_brid=0100007FE7D7F35241946D1E02030303", "st_brid").to_hex_string).to eq("0100007FE7D7F35241946D1E02030303")
19
+ expect(WebServerUid.from_header("baz=0100007FE7D7F35241946D1E02030303", "st_brid")).to be_nil
20
+ expect(WebServerUid.from_header("st_brid=0100007FE7D7F35241946D1E0203030Q", "st_brid")).to be_nil
21
+ expect(WebServerUid.from_header("st_brid=0100007FE7D7F35241946D1E020303", "st_brid")).to be_nil
22
+ end
23
+ end
24
+
25
+ describe "generating a brand-new instance" do
26
+ before :each do
27
+ @generated = WebServerUid.generate
28
+ end
29
+
30
+ it "should be able to create a new instance" do
31
+ expect(@generated).to be_instance_of(WebServerUid)
32
+ end
33
+
34
+ it "should have the right time" do
35
+ expect((Time.now.to_i - @generated.issue_time.to_i).abs).to be < 300
36
+ end
37
+
38
+ it "should have the right IP" do
39
+ require 'socket'
40
+ expected_ipaddr_string = UDPSocket.open {|s| s.connect('8.8.8.8', 1); s.addr.last }
41
+ expected_ipaddr = IPAddr.new(expected_ipaddr_string)
42
+
43
+ expect(@generated.service_number_as_ip).to eq(expected_ipaddr)
44
+ end
45
+
46
+ it "should let you override the IP" do
47
+ @generated = WebServerUid.generate(:ip_address => "127.0.0.1")
48
+ expect(@generated.to_hex_string).to match(/^0100007F/)
49
+ end
50
+
51
+ it "should fail if passed unknown options" do
52
+ expect { WebServerUid.generate(:foo => :bar) }.to raise_error(ArgumentError)
53
+ end
54
+
55
+ it "should fail if passed something that isn't an IP address" do
56
+ expect { WebServerUid.generate(:ip_address => /foobar/) }.to raise_error(ArgumentError)
57
+ end
58
+
59
+ it "should have the right PID" do
60
+ expect(@generated.pid).to eq(Process.pid)
61
+ end
62
+
63
+ it "should have the right sequencer" do
64
+ expect(@generated.sequencer).to be >= 0x030303
65
+ expect(@generated.sequencer).to be <= (0x030303 + 50)
66
+ end
67
+
68
+ it "should have the right version" do
69
+ expect(@generated.cookie_version_number).to eq(2)
70
+ end
71
+
72
+ it "should never generate the same ID twice" do
73
+ ids = [ ]
74
+ 1000.times { ids << WebServerUid.generate }
75
+ expect(ids.map(&:to_hex_string).uniq.length).to eq(1000)
76
+ end
77
+ end
78
+
79
+ describe "known examples" do
80
+ { :hex => '0100007FE7D7F35241946D1E02030303', :base64 => 'fwAAAVLz1+cebZRBAwMDAgS=',
81
+ :binary => "\177\000\000\001R\363\327\347\036m\224A\003\003\003\002" }.each do |type, raw|
82
+ describe type.to_s do
83
+ let(:raw) { @raw }
84
+ let(:uid) { WebServerUid.new(raw, type) }
85
+
86
+ it "should have the right hex string" do
87
+ expect(uid.to_hex_string).to eq('0100007FE7D7F35241946D1E02030303')
88
+ end
89
+
90
+ it "should have the right Base64 string" do
91
+ expect(uid.to_base64_string).to eq("fwAAAVLz1+cebZRBAwMDAg==\n")
92
+ end
93
+
94
+ it "should have the right binary string" do
95
+ actual = uid.to_binary_string
96
+ actual.force_encoding(Encoding::BINARY) if actual.respond_to?(:force_encoding)
97
+ expected = "\177\000\000\001R\363\327\347\036m\224A\003\003\003\002"
98
+ expected.force_encoding(Encoding::BINARY) if expected.respond_to?(:force_encoding)
99
+
100
+ expect(actual).to eq(expected)
101
+ if uid.to_binary_string.respond_to?(:encoding)
102
+ expect(uid.to_binary_string.encoding).to eq(Encoding::BINARY)
103
+ end
104
+ end
105
+
106
+ it "should have the right binary components" do
107
+ expect(uid.binary_components).to eq([ 2130706433, 1391712231, 510497857, 50529026 ])
108
+ end
109
+
110
+ it "should have the right extra binary data" do
111
+ if type == :base64
112
+ expect(uid.extra_binary_data).to eq("\004")
113
+ else
114
+ expect(uid.extra_binary_data).to eq("")
115
+ end
116
+ end
117
+
118
+ it "should have the right service number" do
119
+ expect(uid.service_number).to eq(2130706433)
120
+ end
121
+
122
+ it "should have the right server IP" do
123
+ expect(uid.service_number_as_ip).to eq(IPAddr.new('127.0.0.1'))
124
+ end
125
+
126
+ it "should have the right issue time" do
127
+ expect(uid.issue_time).to eq(1391712231)
128
+ end
129
+
130
+ it "should have the right issue time as time" do
131
+ expect(uid.issue_time_as_time).to eq(Time.parse('Thu Feb 06 10:43:51 -0800 2014'))
132
+ end
133
+
134
+ it "should have the right PID component" do
135
+ expect(uid.pid_component).to eq(510497857)
136
+ end
137
+
138
+ it "should have the right PID" do
139
+ expect(uid.pid).to eq(37953)
140
+ end
141
+
142
+ it "should have the right sequencer component" do
143
+ expect(uid.sequencer_component).to eq(50529026)
144
+ end
145
+
146
+ it "should have the right sequencer value" do
147
+ expect(uid.sequencer).to eq(197379)
148
+ end
149
+
150
+ it "should have the right sequencer, as hex" do
151
+ expect(uid.sequencer_as_hex).to eq("030303")
152
+ end
153
+
154
+ it "should have the right cookie version number" do
155
+ expect(uid.cookie_version_number).to eq(2)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "comparison and hashing" do
162
+ let(:example_1) { WebServerUid.from_hex('0100007FE7D7F35241946D1E02030303') }
163
+ let(:example_2) { WebServerUid.from_hex('0100007FE7D7F35241946D1E02030304') }
164
+ let(:example_3) { WebServerUid.from_hex('0100007FE7D7F35241946D1E02030303') }
165
+ let(:example_4) { WebServerUid.from_hex('0100006FE7D7F35241946D1E02030303') }
166
+
167
+ it "should compare itself with <=> correctly" do
168
+ expect(example_1 <=> example_2).to be < 0
169
+ expect(example_1 <=> example_3).to eq(0)
170
+ expect(example_1 <=> example_4).to be > 0
171
+ expect(example_2 <=> example_3).to be > 0
172
+ expect(example_2 <=> example_4).to be > 0
173
+ expect(example_3 <=> example_4).to be > 0
174
+
175
+ expect(example_2 <=> example_1).to be > 0
176
+ expect(example_3 <=> example_1).to eq(0)
177
+ expect(example_4 <=> example_1).to be < 0
178
+ expect(example_3 <=> example_2).to be < 0
179
+ expect(example_4 <=> example_2).to be < 0
180
+ expect(example_4 <=> example_3).to be < 0
181
+
182
+ expect(example_1.eql?(example_1)).to be_true
183
+ expect(example_1.eql?(example_3)).to be_true
184
+ expect(example_3.eql?(example_1)).to be_true
185
+ expect(example_1.eql?(example_2)).to_not be_true
186
+ expect(example_2.eql?(example_1)).to_not be_true
187
+ end
188
+
189
+ it "should hash itself correctly" do
190
+ expect(example_1.hash).to eq(example_1.hash)
191
+ expect(example_1.hash).to eq(example_3.hash)
192
+ expect(example_2.hash).to eq(example_2.hash)
193
+ expect(example_3.hash).to eq(example_3.hash)
194
+ expect(example_4.hash).to eq(example_4.hash)
195
+ end
196
+ end
197
+
198
+ (0..99).each do |index|
199
+ describe "random example #{index}" do
200
+ before :each do
201
+ @service_number = rand(2**32)
202
+ @issue_time = rand(2**31)
203
+ @issue_time_as_time = Time.at(@issue_time)
204
+ @pid_high = rand(2**16)
205
+ @pid = rand(2**16)
206
+ @sequencer = rand(2**24)
207
+ @version = rand(256)
208
+ @extra = rand(2) == 0 || true ? (65 + rand(26)).chr : ''
209
+
210
+ @components = [
211
+ @service_number,
212
+ @issue_time,
213
+ (@pid_high << 16) | @pid,
214
+ (@sequencer << 8) | @version
215
+ ]
216
+
217
+ @binary = @components.pack("NNNN")
218
+ @hex = @components.pack("VVVV").bytes.map { |b| "%02X" % b}.join("")
219
+ @base64_base = Base64.encode64(@components.pack("NNNN"))
220
+ @base64 = begin
221
+ out = @base64_base.dup
222
+ if @extra.length > 0
223
+ if out =~ /^(.*?)(=*)$/
224
+ out = "#{$1}#{@extra}#{$2}"
225
+ end
226
+ end
227
+ out
228
+ end
229
+ end
230
+
231
+ [ :hex, :base64, :binary ].each do |type|
232
+ describe type.to_s do
233
+ before :each do
234
+ @raw = instance_variable_get("@#{type}")
235
+ @uid = WebServerUid.new(@raw, type)
236
+ end
237
+
238
+ it "should have the right hex string" do
239
+ expect(@uid.to_hex_string).to eq(@hex)
240
+ end
241
+
242
+ it "should have the right Base64 string" do
243
+ expect(@uid.to_base64_string).to eq(@base64_base)
244
+ end
245
+
246
+ it "should have the right binary string" do
247
+ expect(@uid.to_binary_string).to eq(@binary)
248
+ if @uid.to_binary_string.respond_to?(:encoding)
249
+ expect(@uid.to_binary_string.encoding).to eq(Encoding::BINARY)
250
+ end
251
+ end
252
+
253
+ it "should have the right binary components" do
254
+ expect(@uid.binary_components).to eq(@components)
255
+ end
256
+
257
+ it "should have the right extra binary data" do
258
+ if type == :base64 && @extra.length > 0
259
+ actual_extra = Base64.decode64(@base64)[16..-1]
260
+ expect(@uid.extra_binary_data).to eq(actual_extra)
261
+ else
262
+ expect(@uid.extra_binary_data).to eq("")
263
+ end
264
+ end
265
+
266
+ it "should have the right service number" do
267
+ expect(@uid.service_number).to eq(@service_number)
268
+ end
269
+
270
+ it "should have the right server IP" do
271
+ expect(@uid.service_number_as_ip).to eq(IPAddr.new(@service_number, Socket::AF_INET))
272
+ end
273
+
274
+ it "should have the right issue time" do
275
+ expect(@uid.issue_time).to eq(@issue_time)
276
+ end
277
+
278
+ it "should have the right issue time as time" do
279
+ expect(@uid.issue_time_as_time).to eq(@issue_time_as_time)
280
+ end
281
+
282
+ it "should have the right PID component" do
283
+ expect(@uid.pid_component).to eq(@components[2])
284
+ end
285
+
286
+ it "should have the right PID" do
287
+ expect(@uid.pid).to eq(@pid)
288
+ end
289
+
290
+ it "should have the right sequencer component" do
291
+ expect(@uid.sequencer_component).to eq(@components[3])
292
+ end
293
+
294
+ it "should have the right sequencer value" do
295
+ expect(@uid.sequencer).to eq(@sequencer)
296
+ end
297
+
298
+ it "should have the right sequencer, as hex" do
299
+ expect(@uid.sequencer_as_hex).to eq("%06x" % @sequencer)
300
+ end
301
+
302
+ it "should have the right cookie version number" do
303
+ expect(@uid.cookie_version_number).to eq(@version)
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'web_server_uid/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "web_server_uid"
8
+ spec.version = WebServerUid::VERSION
9
+ spec.authors = ["Andrew Geweke"]
10
+ spec.email = ["ageweke@swiftype.com"]
11
+ spec.summary = %q{Parse and represent UID tokens from Apache's mod_uid / nginx's ngx_http_userid_module in Ruby.}
12
+ spec.homepage = "http://www.github.com/swiftype/web_server_uid"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.5"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "rspec", "~> 2.14"
23
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: web_server_uid
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Geweke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2014-02-15 00:00:00 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ prerelease: false
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: "1.5"
22
+ type: :development
23
+ version_requirements: *id001
24
+ - !ruby/object:Gem::Dependency
25
+ name: rake
26
+ prerelease: false
27
+ requirement: &id002 !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - &id004
30
+ - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id002
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id003 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ version: "2.14"
43
+ type: :development
44
+ version_requirements: *id003
45
+ description:
46
+ email:
47
+ - ageweke@swiftype.com
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files: []
53
+
54
+ files:
55
+ - .gitignore
56
+ - .travis.yml
57
+ - Gemfile
58
+ - LICENSE.txt
59
+ - README.md
60
+ - Rakefile
61
+ - lib/web_server_uid.rb
62
+ - lib/web_server_uid/version.rb
63
+ - spec/web_server_uid_spec.rb
64
+ - web_server_uid.gemspec
65
+ homepage: http://www.github.com/swiftype/web_server_uid
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - *id004
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - *id004
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 2.0.14
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Parse and represent UID tokens from Apache's mod_uid / nginx's ngx_http_userid_module in Ruby.
88
+ test_files:
89
+ - spec/web_server_uid_spec.rb