Created
July 27, 2012 15:28
-
-
Save pullmonkey/3188668 to your computer and use it in GitHub Desktop.
USB MagTek Card Reader in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rubygems' | |
require 'usb' # this is the ruby-usb gem, I'm also using libusb 1.0 and linux | |
require 'logger' | |
class MagTek | |
def initialize | |
@device = find_device | |
interface = @device.interfaces.first | |
@endpoint = interface.endpoints.first | |
@logger = Logger.new("/var/log/monitor_usb.log") | |
@logger.level = Logger::INFO | |
end | |
def open | |
close | |
begin | |
@handle = @device.open | |
@handle.usb_detach_kernel_driver_np(0,0) rescue nil | |
@handle.usb_claim_interface(0) | |
@open = true | |
return true | |
rescue | |
return false | |
end | |
end | |
def close | |
return true unless @open | |
@handle.usb_release_interface(0) | |
@handle.usb_close | |
@open = false | |
true | |
end | |
def read(options={}) | |
@logger.debug "Reading from USB ... " | |
clear_swipes | |
data = "" | |
char = (0..7).to_a.pack("c*") | |
begin | |
# read in one character first | |
size = @handle.usb_interrupt_read(@endpoint.bEndpointAddress, char, 0) | |
data += char | |
@logger.debug "Read in char: #{char.inspect}" | |
converted_data = convert_data(data) | |
@logger.debug "Data is now: #{clean_data(converted_data)}" | |
# then read in 1 char at a time until we reach the new line at the end | |
until (converted_data = convert_data(data)).include? "\\n" do | |
begin | |
size = @handle.usb_bulk_read(@endpoint.bEndpointAddress, char, -1) | |
rescue Errno::ETIMEDOUT => e | |
# nothing ... we know it is going to time out, that's the point | |
end | |
data += char | |
end | |
converted_data = convert_data(data) | |
@logger.debug "Data is now: #{clean_data(converted_data)}" | |
rescue Errno::EBUSY => e | |
@logger.error e.message | |
@logger.error "Will try to detach the kernel driver." | |
@handle.usb_detach_kernel_driver_np(0,0) rescue nil | |
return false | |
rescue Exception => e | |
@logger.error "could not read from USB. Will try to reset it." | |
@logger.error e.message | |
begin | |
@handle.usb_reset() | |
rescue Exception => e2 | |
@logger.error e2.message | |
@logger.error "Could not reset, make sure device is plugged in. And then restart usb monitor daemon." | |
@logger.error "Exiting" | |
exit(1) | |
end | |
@handle.usb_detach_kernel_driver_np(0,0) rescue nil | |
return false | |
end | |
data = convert_data(data) | |
name = get_name(data) | |
drivers_license_number = get_drivers_license_number(data) | |
if name or drivers_license_number | |
return [true, name, drivers_license_number] | |
else | |
begin | |
@handle.usb_reset() | |
rescue Exception => e | |
@logger.error e.message | |
@logger.error "Could not reset, make sure device is plugged in. Or unplug and replug device." | |
@logger.error "Exiting" | |
exit(1) | |
end | |
return false | |
end | |
end | |
private | |
# clean the pipes | |
def clear_swipes | |
@logger.debug "Clearing swipes" | |
data = (0..7).to_a.pack("c*") | |
# used to be 50.times with just snl badge | |
10.times do |x| | |
begin | |
@handle.usb_interrupt_read(@endpoint.bEndpointAddress, data, -1) | |
rescue Errno::ETIMEDOUT => e | |
# seeing a new line usually means we've reached the end | |
if ["\\n"].include?(char = convert_data(data)) | |
@logger.debug "Done clearing swipes: reached blank or new line: #{char.inspect}" | |
return | |
end | |
@logger.debug "Timed out while clearing" | |
next | |
rescue Exception => e | |
@logger.debug "Done clearing swipes: #{e.message}" | |
return | |
end | |
end | |
@logger.debug "Done clearing swipes: reached end of count" | |
end | |
def get_name(data) | |
@logger.debug "Getting Name: DATA: #{clean_data(data).inspect}" | |
# match for drivers license | |
match = /%.*?\^(.*\$.*\$.*?)\^.*?\^/.match(data) | |
return nil unless match | |
name = match.captures.first.gsub(/\$/, " ") | |
@logger.debug "Got name: #{name}" | |
return name | |
end | |
def get_drivers_license_number(data) | |
@logger.debug "Getting Drivers License Number: DATA: #{clean_data(data).inspect}" | |
# match for drivers license | |
match = /\?;\d{6}(\d+)=\d+/.match(data) | |
return nil unless match | |
lic_num = match.captures.first | |
lic_num = lic_num[-4..-1] # only last 4 digits | |
@logger.debug "Got lic. num: #{lic_num}" | |
return lic_num | |
end | |
# some cards we use have SSN on them, definitely don't want that in the logs, so clear it out | |
def clean_data(data) | |
ssn = nil | |
if data =~ /%(\d{9})/ | |
ssn = $1 | |
end | |
data = data.gsub(ssn, "SSN-not-loggable") if ssn | |
return data | |
end | |
def find_device | |
return USB.devices.find{|u| u.idProduct == 0x0001 && u.idVendor == 0x0801} | |
end | |
def convert_data_slices(data) | |
data.map do |d| | |
d[0].to_i == 2 ? key_pages_shift[d[2].to_i] : key_pages[d[2].to_i] | |
end.join("") | |
end | |
def convert_data(data) | |
data = data.unpack('C*') | |
convert_data_slices(data.each_slice(8).to_a) | |
end | |
# keycode mapping, we seem to be 1 character off otherwise :( | |
# by some manner of luck, I discovered this mapping here - https://github.com/guyzmo/tmsr33-pyusb/blob/master/tmsr33_pyusb.py | |
def key_pages | |
[ | |
'', '', '', '', | |
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', | |
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', | |
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\n', '^]', '^H', | |
'^I', ' ', '-', '=', '[', ']', '\\', '>', ';', "'", '`', ',', '.', | |
'/', 'CapsLock', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', | |
'PS', 'SL', 'Pause', 'Ins', 'Home', 'PU', '^D', 'End', 'PD', '->', '<-', '-v', '-^', 'NL', | |
'KP/', 'KP*', 'KP-', 'KP+', 'KPE', 'KP1', 'KP2', 'KP3', 'KP4', 'KP5', 'KP6', 'KP7', 'KP8', | |
'KP9', 'KP0', '\\', 'App', 'Pow', 'KP=', 'F13', 'F14' ] | |
end | |
def key_pages_shift | |
[ | |
'', '', '', '', | |
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', | |
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', | |
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '\n', '^]', '^H', | |
'^I', ' ', '_', '+', '{', '}', '|', '<', ':', '"', '~', '<', '>', | |
'?', 'CapsLock', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', | |
'PS', 'SL', 'Pause', 'Ins', 'Home', 'PU', '^D', 'End', 'PD', '->', '<-', '-v', '-^', 'NL', | |
'KP/', 'KP*', 'KP-', 'KP+', 'KPE', 'KP1', 'KP2', 'KP3', 'KP4', 'KP5', 'KP6', 'KP7', 'KP8', | |
'KP9', 'KP0', '|', 'App', 'Pow', 'KP=', 'F13', 'F14' ] | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment