web_server_uid 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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