This module allows you to send OAuth-signed HTTP requests to any API you like. With this module, your games will be able to do stuff like signing in to twitter as apps and post status updates or retrieve the user's timeline.
I still haven't fully tested it with post/get params and everything, so feel free to post any issue in this thread.
EDIT: just did some more in-depth testing and managed to retrieve my twitter timeline. still haven't tested it with POST though
EDIT: tested GET and POST params and they seem to be working after this last bugfix :D
Why would you need something like that on RPGMaker?
I'm a programming enthusiast, and I usually have no use for pre-made engines like RPGMaker because for me most of the fun is making the engine from scratch in low-ish level languages such as C and C++.
About two days ago I had a great idea which proved to be a ton of fun: messing around with RPGMaker's scripting system to see if I could make it do things it wasn't exactly designed for in pure RGSS3 (without using custom dlls) and made a script that successfully gets an OAuth token from twitter.
This is the OAuth module I wrote for this purpose.
Author: Franc[e]sco aka lolisamurai
Required scripts: TextUtils (http://hnng.moe/f/hq), Base64 (http://hnng.moe/f/hr), Digest (http://www.rpgmakervxace.net/topic/29543-hmac-sha1-digesthashing-module/)
Download: http://hnng.moe/f/iC (http://hnng.moe/f/iC)
Source code (also on pastie (http://pastie.org/9823263)):
[spoiler]
=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
[/spoiler]
Video demonstration of my module obtaining an OAuth token from twitter: http://hnng.moe/f/ho (http://hnng.moe/f/ho)
Example code - obtaining an OAuth token from twitter:
html = OAuth::request(
"https://api.twitter.com/oauth/request_token",
"GET", Hash[], "my_consumer_key",
"my_consumer_secret"
)
oauth_token = ""
oauth_token_secret = ""
html.split(/&/).each do |param|
name, value = param.split(/=/)
if name == "oauth_token"
oauth_token = value
elsif name == "oauth_token_secret"
oauth_token_secret = value
end
end
authurl = "api.twitter.com/oauth/authorize?oauth_token="
authurl += oauth_token
Thread.new { system("start https://#{authurl}") }
The consumer key and consumer secret are provided by twitter when you create your custom app.
Updated!
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
Updated yet again, GET params are now working great! POST should also work fine.
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.