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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +6 -0
- data/lib/web_server_uid/version.rb +3 -0
- data/lib/web_server_uid.rb +297 -0
- data/spec/web_server_uid_spec.rb +309 -0
- data/web_server_uid.gemspec +23 -0
- metadata +89 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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 — 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 — 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: 
|
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 — 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 — `<`, `<=`, `>`, `>=`, `<=>`, `==`, `!=`, and `eql?` — 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,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
|