Skip to content

Instantly share code, notes, and snippets.

@pullmonkey
Created July 27, 2012 15:28
Show Gist options
  • Save pullmonkey/3188668 to your computer and use it in GitHub Desktop.
Save pullmonkey/3188668 to your computer and use it in GitHub Desktop.
USB MagTek Card Reader in Ruby
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