=begin
OAuth r2
by Franc[e]sco
Requires: TextUtils, Base64, Digest
01/09/2015
r0
[+] Initial revision
r1
[*] Fixed the call to build_signed_parameters which was missing one argument
which caused the parameters to incorrectly build when using a token or
a pin
01/10/2015
r2
[*] Fixed WinHttpSendRequest not taking a pointer as the post param data
and causing errors when using post params.
[*] Modified some logic that flawed due to the wrong assumption that
WinHttpCrackUrl behaved the same when not dynamically allocating
the results.
[+] Added debug toggle and some debugging printfs.
=end
DEBUG = false
# WinAPI
GetLastError = Win32API.new("Kernel32", "GetLastError", "", "I")
WinHttpOpen = Win32API.new("winhttp", "WinHttpOpen", "PIPPI", "I")
WinHttpConnect = Win32API.new("winhttp", "WinHttpConnect", "PPII", "I")
WinHttpAddRequestHeaders = Win32API.new("winhttp",
"WinHttpAddRequestHeaders", "PPII", "I")
WinHttpOpenRequest = Win32API.new("winhttp",
"WinHttpOpenRequest", "PPPPPPI", "I")
WinHttpSendRequest = Win32API.new("winhttp",
"WinHttpSendRequest", "PIIPIII", "I")
WinHttpReceiveResponse = Win32API.new("winhttp",
"WinHttpReceiveResponse", "PP", "I")
WinHttpQueryDataAvailable = Win32API.new("winhttp",
"WinHttpQueryDataAvailable", "PP", "I")
WinHttpReadData = Win32API.new("winhttp", "WinHttpReadData", "PPIP", "I")
WinHttpCloseHandle = Win32API.new("winhttp", "WinHttpCloseHandle", "P", "I")
WINHTTP_FLAG_SECURE = 0x00800000
ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
# module OAuth contains utilities to send signed HTTP requests with OAuth
module OAuth
# OAuth::request sends a OAuth-signed web request
#
# Parameters:
# url: full url
# method: GET/POST
# post_params: Hash that contains post parameters mapped by name
# (e.g. Hash["my_value" => 10, "other_value" => "hello"])
# oauth_consumer_key, oauth_consumer_secret: these must always be provided
# and they identify your application. the service on which you're trying
# to authenticate should have provided you with these when registering
# your application.
# oauth_token, oauth_token_secret (optional): must be provided on every call
# except for the very first request where you actually get these tokens by
# requesting them.
# pin (optional): used during authorization. after obtaining oauth_token and
# oauth_token_secret, the user will be prompted to authorize your
# application and will be given a pin to enter into your software.
#
# Returns: the contents of the response as plain text
def self.request(url, method, post_params, oauth_consumer_key,
oauth_consumer_secret, oauth_token="", oauth_token_secret="", pin="")
gle = 0
signed_params = ""
loop do
query, gle = TextUtils::url_get_query(url)
break unless gle == 0
get_params = TextUtils::parse_query(query)
signed_params, gle = build_signed_parameters(get_params, url, method,
post_params, oauth_consumer_key, oauth_consumer_secret, oauth_token,
oauth_token_secret, pin)
break
end
unless gle == 0
return sprintf("%s, GLE=%.8X (%d)", signed_params, gle, gle)
end
return signed_request(signed_params, url, method, post_params)
end
# ----------------------------------------------------------------------------
# [ Internal module functions ]
# you most likely don't need to use the stuff below here or understand it
# unless you know what you're doing
# ----------------------------------------------------------------------------
# generates the OAuth signature from the signature base and the consumer
# secret. a request token secret can optionally be provided
def self.create_signature(signature_base,
consumer_secret, request_token_secret = "")
escaped_consumer_secret = TextUtils::url_encode(consumer_secret)
escaped_token_secret = TextUtils::url_encode(request_token_secret)
key = escaped_consumer_secret + "&" + escaped_token_secret
hash = ""
loop do
hash, gle = Digest::hmacsha1(key, signature_base)
if gle != 0
return [hash, gle]
end
# sometimes hash will be an empty string for no apparent reason
# but retrying eventually fixes it
# this is probabilly because of my super ghetto hashing code
break unless hash.empty?
end
signature = Base64.encode(hash)[0..-2]
signature = TextUtils::url_encode(signature)
return [signature, 0]
end
private_class_method :create_signature
# returns the normalized sorted oauth parameters as a string
def self.normalize_parameters(params)
res = ""
first = true
params.sort.map do |key, value|
res += "&" unless first
res += sprintf("%s=%s", key, value)
first = false
end
return res
end
private_class_method :normalize_parameters
# normalizes the given url
def self.normalize_url(url)
normalized = ""
gle = 0
loop do
host, path, extrainfo, port, gle = TextUtils::url_crack(url)
unless gle == 0
break
end
# specify non-standard port
port_str = ""
if (url.start_with?("http://") and port != 80) or
(url.start_with?("https://") and port != 443)
port_str = sprintf(":%u", port)
end
# TODO: edit url_crack to also return the scheme
# instead of parsing it like this
scheme = "invalid://"
s = url.index("://")
scheme = url[0, s] unless s == nil
normalized = scheme + "://" + host + port_str + path
break
end
return normalized
end
private_class_method :normalize_url
# creates a oauth-signed parameter list for the desired request
def self.build_signed_parameters(get_params, url, method, post_params,
oauth_consumer_key, oauth_consumer_secret, oauth_token="",
oauth_token_secret="", pin="")
# prepare oauth params
oauth_params = Hash[
"oauth_consumer_key" => oauth_consumer_key,
"oauth_nonce" => TextUtils::random_string(ALPHANUMERIC, 32),
"oauth_signature_method" => "HMAC-SHA1",
"oauth_timestamp" => Time.now.to_i,
"oauth_version" => "1.0"
]
# optional params
oauth_params["oauth_token"] = oauth_token unless oauth_token.empty?
oauth_params["oauth_verifier"] = pin unless pin.empty?
# create a full param list with the GET/POST stuff and the oauth params
all_params = get_params
all_params.merge!(post_params) if method == "POST" and post_params.length > 0
all_params.merge!(oauth_params)
# prepare signature base
normalized_url = normalize_url(url)
normalized_params = normalize_parameters(all_params)
signature_base = sprintf("GET&%s&%s",
TextUtils::url_encode(normalized_url),
TextUtils::url_encode(normalized_params))
printf("signature_base = %s\n", signature_base) if DEBUG
oauth_signature, gle = create_signature(signature_base,
oauth_consumer_secret, oauth_token_secret)
if gle != 0
return [oauth_signature, gle]
end
oauth_params["oauth_signature"] = oauth_signature
return [oauth_params, 0]
end
private_class_method :build_signed_parameters
# build oauth header from the signed oauth params
def self.build_header(params)
oauth_header = "Authorization: OAuth "
firstparam = true
params.sort.map do |key, value|
oauth_header += ", " unless firstparam
oauth_header += sprintf('%s="%s"', key, value)
firstparam = false
end
return oauth_header
end
private_class_method :build_header
# sends a signed http request with the given signed params
def self.signed_request(signed_params, url, method, post_params)
session = 0
connect = 0
request = 0
html = ""
host, path, extrainfo, port, gle = TextUtils::url_crack(url)
loop do
session = WinHttpOpen.call("RPG Maker VX Ace/1.0", 0, '', '', 0)
unless session
html = sprintf("Failed to open session, GLE=0x%.8X", GetLastError.call)
break
end
connect = WinHttpConnect.call(session, TextUtils::to_ws(host), port, 0)
unless connect
html = sprintf("Failed to connect to %s:%d, GLE=0x%.8X",
host, port, GetLastError.call)
break
end
printf("%s %s\n", method, path + extrainfo) if DEBUG
request = WinHttpOpenRequest.call(connect, TextUtils::to_ws(method),
TextUtils::to_ws(path + extrainfo), TextUtils::to_ws("HTTP/1.1"),
"", nil, WINHTTP_FLAG_SECURE)
unless request
html = sprintf("Failed to open request %s %s, GLE=0x%.8X",
method, path, GetLastError.call)
break
end
# add oauth header
oauth_header = build_header(signed_params)
printf("oauth_header = %s\n", oauth_header) if DEBUG
headerok = WinHttpAddRequestHeaders.call(request,
TextUtils::to_ws(oauth_header), oauth_header.length, 0)
unless headerok
html = sprintf("Failed to add oauth header \"%s\", GLE=0x%.8X",
oauth_header, GetLastError.call)
break
end
# add post headers if needed
postdata = ""
if method == "POST" and post_params.length > 0
content_header = "Content-Type: application/x-www-form-urlencoded\r\n"
content_header_ok = WinHttpAddRequestHeaders.call(request,
TextUtils::to_ws(content_header), content_header.length, 0)
unless headerok
html = sprintf("Failed to add content header \"%s\", GLE=0x%.8X",
content_header, GetLastError.call)
break
end
postdata = TextUtils::build_query(post_params)
end
# send request
results = WinHttpSendRequest.call(request, 0, 0,
postdata.length > 0 ? postdata : "", postdata.length,
postdata.length, 0)
unless results
html = sprintf(
"Failed to send request with postdata \"%s\", GLE=0x%.8X",
postdata, GetLastError.call)
break
end
# get and read response
results = WinHttpReceiveResponse.call(request, nil)
unless results
html = sprintf("Failed to get response, GLE=0x%.X", GetLastError.call)
break
end
loop do
size = 0
# check how much data is available and dynamically allocate a buffer
unless WinHttpQueryDataAvailable.call(request, o=[size].pack('i!'))
html = sprintf("Failed to query for available data, GLE=0x%.X",
GetLastError.call)
break
end
size = o.unpack('i!')[0]
buffer = ' ' * size
downloaded = 0
# read data into the buffer
unless WinHttpReadData.call(request, buffer,
size, o=[downloaded].pack('i!'))
html = sprintf(
"Failed to read %d bytes of available data, GLE=0x%.8X",
size, GetLastError.call)
break
end
downloaded = o.unpack('i!')[0]
# append data to html
html << buffer
# keep reading data as long as there is some
break unless size > 0
end
# clean up
WinHttpCloseHandle.call(request) if request
WinHttpCloseHandle.call(connect) if connect
WinHttpCloseHandle.call(session) if session
break
end
return html
end
private_class_method :signed_request
end