├── unittest ├── test_twitter_multipart.jpg ├── luanode │ ├── run.cmd │ ├── run.sh │ ├── tests │ │ ├── net_error.lua │ │ └── echo_lab_madgex_com.lua │ └── run.lua ├── run.lua ├── net_error.lua ├── google.lua ├── echo_lab_madgex_com.lua ├── twitter.lua └── termie.lua ├── .travis └── setup_luanode.sh ├── Makefile ├── rockspec ├── oauth-scm-1.rockspec ├── oauth-0.0.1-1.rockspec ├── oauth-0.0.2-1.rockspec ├── oauth-0.0.4-1.rockspec ├── oauth-git-1.rockspec ├── oauth-0.0.3-1.rockspec ├── oauth-0.0.5-1.rockspec └── oauth-0.0.6-1.rockspec ├── .travis.yml ├── LICENSE ├── src ├── OAuth │ ├── helpers.lua │ ├── coreLuaSocket.lua │ └── coreLuaNode.lua ├── OAuth2.lua └── OAuth.lua └── README.md /unittest/test_twitter_multipart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignacio/LuaOAuth/HEAD/unittest/test_twitter_multipart.jpg -------------------------------------------------------------------------------- /unittest/luanode/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | for %%f in (tests\*.lua) do ( 4 | echo %%f 5 | call luanode run.lua %%f 6 | if errorlevel 1 goto :eof 7 | if not errorlevel -1 goto :eof 8 | ) 9 | 10 | echo Ended without errors 11 | -------------------------------------------------------------------------------- /.travis/setup_luanode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | # 3 | # install luanode dependencies 4 | # 5 | git clone --depth=1 --branch=master git://github.com/ignacio/LuaNode.git ~/luanode 6 | cd ~/luanode/build 7 | cmake -DBOOST_ROOT=/usr/lib .. 8 | cmake --build . 9 | ln -s ~/luanode/build/luanode $LUA_DIR/bin/luanode 10 | luanode -v 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | LUA := lua 3 | 4 | ifndef DESTDIR 5 | DESTDIR := /usr/local 6 | endif 7 | LIBDIR := $(DESTDIR)/share/lua/5.1 8 | 9 | install: 10 | cp src/OAuth.lua $(LIBDIR) 11 | 12 | uninstall: 13 | rm -f $(LIBDIR)/OAuth.lua 14 | 15 | export LUA_PATH=;;src/?.lua;unittest/?.lua 16 | 17 | test: 18 | $(LUA) unittest/run.lua 19 | -------------------------------------------------------------------------------- /unittest/run.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | 3 | package.path = "../src/?.lua;../src/?/init.lua;".. package.path 4 | 5 | require "echo_lab_madgex_com" 6 | --require "twitter" 7 | require "termie" 8 | require "google" 9 | require "net_error" 10 | 11 | 12 | local stats = lunit.main() 13 | if stats.errors > 0 or stats.failed > 0 then 14 | os.exit(1) 15 | end 16 | -------------------------------------------------------------------------------- /unittest/luanode/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -o pipefail 4 | IFS=$'\n\t' 5 | 6 | declare -i has_error=0 7 | 8 | for i in $( ls -1 tests/*.lua ); do 9 | echo -e "\033[32mRunning test case: tests."$i"\033[0m" 10 | luanode run.lua $i 11 | if [ $? -ne 0 ] 12 | then 13 | echo -e "\033[1m\033[41mTest case failed: " $i "\033[0m" 14 | has_error=1 15 | fi 16 | done 17 | 18 | if [ $has_error -eq 0 ]; then 19 | echo "Ended without errors" 20 | else 21 | echo "Ended with errors" 22 | fi 23 | 24 | exit $has_error 25 | -------------------------------------------------------------------------------- /unittest/net_error.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | local OAuth = require "OAuth" 3 | 4 | if _VERSION >= "Lua 5.2" then 5 | _ENV = lunit.module("simple","seeall") 6 | else 7 | module(..., package.seeall, lunit.testcase) 8 | end 9 | 10 | 11 | function test() 12 | print("\nTest: net error\n") 13 | local client = OAuth.new("anonymous", "anonymous", { 14 | RequestToken = "http://127.0.0.1:12345/foo", 15 | AuthorizeUser = {"http://127.0.0.1:12345/bar", method = "GET"}, 16 | AccessToken = "http://127.0.0.1:12345/baz" 17 | }) 18 | 19 | local ok, err_msg = client:RequestToken() 20 | assert(not ok) 21 | assert_equal("connection refused", err_msg) 22 | end 23 | -------------------------------------------------------------------------------- /rockspec/oauth-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "scm-1" 3 | source = { 4 | url = "git://github.com/ignacio/LuaOAuth.git", 5 | branch = "master" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | OAuth = "src/OAuth.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rockspec/oauth-0.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "0.0.1-1" 3 | source = { 4 | url = "git://github.com/ignacio/LuaOAuth.git", 5 | branch = "v0.0.1" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | OAuth = "src/OAuth.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rockspec/oauth-0.0.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "0.0.2-1" 3 | source = { 4 | url = "git://github.com/ignacio/LuaOAuth.git", 5 | branch = "v0.0.2" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | OAuth = "src/OAuth.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /unittest/luanode/tests/net_error.lua: -------------------------------------------------------------------------------- 1 | local OAuth = require "OAuth" 2 | 3 | module(..., lunit.testcase, package.seeall) 4 | 5 | function test() 6 | local client = OAuth.new("anonymous", "anonymous", { 7 | RequestToken = "http://127.0.0.1:12345/foo", 8 | AuthorizeUser = {"http://127.0.0.1:12345/bar", method = "GET"}, 9 | AccessToken = "http://127.0.0.1:12345/baz" 10 | }) 11 | 12 | local ok, err_msg = client:RequestToken(function(err, values, status, headers, response_line, response_body) 13 | assert(err) 14 | assert_string(err.message) -- error message 15 | assert_number(err.code) -- error code 16 | assert_equal(process.constants.ECONNREFUSED, err.code) 17 | end) 18 | 19 | process:loop() 20 | end 21 | -------------------------------------------------------------------------------- /rockspec/oauth-0.0.4-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "0.0.4-1" 3 | source = { 4 | url = "https://github.com/ignacio/LuaOAuth/archive/0.0.4-1.tar.gz", 5 | dir = "LuaOAuth-0.0.4-1" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | OAuth = "src/OAuth.lua", 30 | ["OAuth.helpers"] = "src/OAuth/helpers.lua", 31 | ["OAuth.coreLuaSocket"] = "src/OAuth/coreLuaSocket.lua", 32 | ["OAuth.coreLuaNode"] = "src/OAuth/coreLuaNode.lua", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rockspec/oauth-git-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "git-1" 3 | source = { 4 | url = "git://github.com/ignacio/LuaOAuth.git", 5 | branch = "master" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | OAuth = "src/OAuth.lua", 30 | ["OAuth.helpers"] = "src/OAuth/helpers.lua", 31 | ["OAuth.coreLuaSocket"] = "src/OAuth/coreLuaSocket.lua", 32 | ["OAuth.coreLuaNode"] = "src/OAuth/coreLuaNode.lua", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rockspec/oauth-0.0.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "0.0.3-1" 3 | source = { 4 | url = "git://github.com/ignacio/LuaOAuth.git", 5 | branch = "v0.0.3" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | build = { 27 | type = "builtin", 28 | modules = { 29 | OAuth = "src/OAuth.lua", 30 | ["OAuth.helpers"] = "src/OAuth/helpers.lua", 31 | ["OAuth.coreLuaSocket"] = "src/OAuth/coreLuaSocket.lua", 32 | ["OAuth.coreLuaNode"] = "src/OAuth/coreLuaNode.lua", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rockspec/oauth-0.0.5-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "0.0.5-1" 3 | source = { 4 | url = "https://github.com/ignacio/LuaOAuth/archive/0.0.5-1.tar.gz", 5 | dir = "LuaOAuth-0.0.5-1" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua ~> 5.1", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | 27 | build = { 28 | type = "builtin", 29 | modules = { 30 | OAuth = "src/OAuth.lua", 31 | ["OAuth.helpers"] = "src/OAuth/helpers.lua", 32 | ["OAuth.coreLuaSocket"] = "src/OAuth/coreLuaSocket.lua", 33 | ["OAuth.coreLuaNode"] = "src/OAuth/coreLuaNode.lua", 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rockspec/oauth-0.0.6-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "OAuth" 2 | version = "0.0.6-1" 3 | source = { 4 | url = "https://github.com/ignacio/LuaOAuth/archive/0.0.6-1.tar.gz", 5 | dir = "LuaOAuth-0.0.6-1" 6 | } 7 | description = { 8 | summary = "Lua OAuth, an OAuth client library.", 9 | detailed = [[ 10 | Lua client for OAuth 1.0 enabled servers. 11 | ]], 12 | license = "MIT/X11", 13 | homepage = "http://github.com/ignacio/LuaOAuth" 14 | } 15 | dependencies = { 16 | "lua >= 5.1, <= 5.2", 17 | "luasocket", 18 | "luasec", 19 | "luacrypto", 20 | "lbase64" 21 | } 22 | 23 | external_dependencies = { 24 | 25 | } 26 | 27 | build = { 28 | type = "builtin", 29 | modules = { 30 | OAuth = "src/OAuth.lua", 31 | ["OAuth.helpers"] = "src/OAuth/helpers.lua", 32 | ["OAuth.coreLuaSocket"] = "src/OAuth/coreLuaSocket.lua", 33 | ["OAuth.coreLuaNode"] = "src/OAuth/coreLuaNode.lua", 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /unittest/google.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | local OAuth = require "OAuth" 3 | 4 | if _VERSION >= "Lua 5.2" then 5 | _ENV = lunit.module("simple","seeall") 6 | else 7 | module(..., package.seeall, lunit.testcase) 8 | end 9 | 10 | 11 | function test() 12 | local client = OAuth.new("anonymous", "anonymous", { 13 | RequestToken = "https://www.google.com/accounts/OAuthGetRequestToken", 14 | AuthorizeUser = {"https://www.google.com/accounts/OAuthAuthorizeToken", method = "GET"}, 15 | AccessToken = "https://www.google.com/accounts/OAuthGetAccessToken" 16 | }) 17 | 18 | local request_token = client:RequestToken({ oauth_callback = "oob", scope = "https://www.google.com/analytics/feeds/" }) 19 | 20 | assert( not string.match(request_token.oauth_token, "%%") ) 21 | print(request_token.oauth_token) 22 | 23 | local auth_url = client:BuildAuthorizationUrl() 24 | print("Test authorization at the following URL") 25 | print(auth_url) 26 | print(string.match(auth_url, "oauth_token=([^&]+)&")) 27 | end 28 | -------------------------------------------------------------------------------- /unittest/luanode/run.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | 3 | package.path = "../../src/?.lua;../../src/?/init.lua;".. package.path 4 | 5 | local console = require "lunit.console" 6 | 7 | -- eventos posibles: 8 | -- begin, done, fail, err, run, pass 9 | 10 | local num_tests = select("#", ...) 11 | if num_tests == 0 then 12 | print("No tests to run...") 13 | return 14 | end 15 | 16 | if num_tests == 1 and select(1, ...) == "all" then 17 | print("TODO: Run all tests") 18 | return 19 | end 20 | 21 | for i=1, num_tests do 22 | local test_case = select(i, ...) 23 | test_case = test_case:gsub("%.lua$", "") 24 | require(test_case) 25 | end 26 | 27 | lunit.setrunner({ 28 | begin = console.begin, 29 | --fail = function(...) 30 | --LogError("Error in test case %s\r\nat %s\r\n%s\r\n%s", ...) 31 | --end, 32 | fail = console.fail, 33 | err = console.err, 34 | --err = function(...) 35 | -- LogError(...) 36 | -- end, 37 | done = function(...) 38 | process:emit("exit") 39 | process:removeAllListeners("exit") 40 | console.done() 41 | end 42 | }) 43 | 44 | --console.begin() 45 | local stats = lunit.run() 46 | if stats.failed > 0 or stats.errors > 0 then 47 | return 1 48 | end 49 | --console.done() -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python # Need python environment for pip 2 | 3 | sudo: false # Use container-based infrastructure 4 | 5 | env: 6 | - LUA="lua=5.1" 7 | - LUA="lua=5.2" 8 | # #- LUA="lua=5.3" (luacrypto does not support Lua 5.3 yet) 9 | - LUA="luajit=2.0" 10 | - LUA="luajit=2.1" 11 | 12 | 13 | matrix: 14 | include: 15 | - env: LUA="lua=5.1" LUANODE=true 16 | addons: 17 | apt: 18 | packages: 19 | - libboost-dev 20 | - libboost-system-dev 21 | - libboost-date-time-dev 22 | - libboost-thread-dev 23 | 24 | 25 | before_install: 26 | - pip install hererocks 27 | - hererocks lua_install --luarocks ^ --$LUA 28 | - export PATH=$PATH:$PWD/lua_install/bin 29 | - | 30 | if [ "$LUANODE" == "true" ]; then 31 | export LUA_DIR="$PWD/lua_install" 32 | bash .travis/setup_luanode.sh 33 | fi 34 | - luarocks install luacov 35 | - luarocks install luacov-coveralls 36 | 37 | install: 38 | - luarocks install lunitx 39 | - luarocks install luacrypto 40 | - luarocks install luasocket 3.0rc1-2 41 | #- luarocks install luasocket --only-server=http://luarocks.org/repositories/rocks-scm 42 | - luarocks install lbase64 43 | - luarocks install luasec --server=https://luarocks.org/manifests/ignacio OPENSSL_LIBDIR=/usr/lib/x86_64-linux-gnu 44 | 45 | script: 46 | - | 47 | if [ "$LUANODE" = "true" ]; then 48 | cd unittest/luanode && ./run.sh; 49 | else 50 | cd unittest && lua -lluacov run.lua; 51 | fi 52 | 53 | after_success: 54 | - luacov-coveralls -v -r .. -e ../lua_install 55 | -------------------------------------------------------------------------------- /unittest/luanode/tests/echo_lab_madgex_com.lua: -------------------------------------------------------------------------------- 1 | local OAuth = require "OAuth" 2 | 3 | module(..., lunit.testcase, package.seeall) 4 | 5 | -- see: http://echo.lab.madgex.com/ 6 | function test() 7 | print("echo.lab.madgex.com") 8 | local client = OAuth.new("key", "secret", { 9 | RequestToken = "http://echo.lab.madgex.com/request-token.ashx", 10 | AccessToken = "http://echo.lab.madgex.com/access-token.ashx" 11 | }) 12 | print("Requesting token") 13 | client:RequestToken(function(err, values) 14 | assert_nil(err) 15 | 16 | assert_table(values) 17 | assert_equal("requestkey", values.oauth_token) 18 | assert_equal("requestsecret", values.oauth_token_secret) 19 | 20 | print("Retrieving access token") 21 | client:GetAccessToken(function(err, values) 22 | 23 | assert_table(values) 24 | assert_equal("accesskey", values.oauth_token) 25 | assert_equal("accesssecret", values.oauth_token_secret) 26 | 27 | print("Making authenticated call") 28 | local client = OAuth.new("key", "secret", { 29 | RequestToken = "http://echo.lab.madgex.com/request-token.ashx", 30 | AccessToken = "http://echo.lab.madgex.com/access-token.ashx" 31 | }) 32 | client:SetToken("accesskey") 33 | client:SetTokenSecret("accesssecret") 34 | client:PerformRequest("GET", "http://echo.lab.madgex.com/echo.ashx", 35 | {foo = "bar"}, 36 | function(err, response_code, response_headers, response_status_line, response_body) 37 | if err then 38 | print("Error making request") 39 | print(err.code) 40 | print(err.message) 41 | return 42 | end 43 | if response_code ~= 200 then 44 | print("Error requesting token:", response_code) 45 | for k,v in pairs(response_headers) do print(k,v) end 46 | print(response_status_line) 47 | print(response_body) 48 | end 49 | print(response_body) 50 | assert_equal("foo=bar", response_body) 51 | end) 52 | end) 53 | end) 54 | 55 | process:loop() 56 | end 57 | -------------------------------------------------------------------------------- /unittest/echo_lab_madgex_com.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | local OAuth = require "OAuth" 3 | 4 | if _VERSION >= "Lua 5.2" then 5 | _ENV = lunit.module("simple","seeall") 6 | else 7 | module(..., package.seeall, lunit.testcase) 8 | end 9 | 10 | -- see: http://echo.lab.madgex.com/ 11 | function test() 12 | print("\nTest: echo.lab.madgex.com\n") 13 | local client = OAuth.new("key", "secret", { 14 | RequestToken = "http://echo.lab.madgex.com/request-token.ashx", 15 | AccessToken = "http://echo.lab.madgex.com/access-token.ashx" 16 | }) 17 | print("Requesting token") 18 | local values, status, headers, response_line, response_body = client:RequestToken() 19 | if not values then 20 | print(status) 21 | for k,v in pairs(headers) do print(k,v) end 22 | print(response_line) 23 | print(response_body) 24 | end 25 | assert_table(values) 26 | assert_equal("requestkey", values.oauth_token) 27 | assert_equal("requestsecret", values.oauth_token_secret) 28 | 29 | print("Retrieving access token") 30 | local values = client:GetAccessToken() 31 | 32 | assert_table(values) 33 | assert_equal("accesskey", values.oauth_token) 34 | assert_equal("accesssecret", values.oauth_token_secret) 35 | 36 | print("Making authenticated call") 37 | local client = OAuth.new("key", "secret", { 38 | RequestToken = "http://echo.lab.madgex.com/request-token.ashx", 39 | AccessToken = "http://echo.lab.madgex.com/access-token.ashx" 40 | }) 41 | client:SetToken("accesskey") 42 | client:SetTokenSecret("accesssecret") 43 | local response_code, response_headers, response_status_line, response_body = client:PerformRequest("GET", "http://echo.lab.madgex.com/echo.ashx", 44 | {foo = "bar"} 45 | ) 46 | if response_code ~= 200 then 47 | print("Error requesting token:", response_code) 48 | for k,v in pairs(response_headers) do print(k,v) end 49 | print(response_status_line) 50 | print(response_body) 51 | end 52 | print(response_body) 53 | assert_equal("foo=bar", response_body) 54 | end 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Ignacio Burgueño 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | 24 | ----------------- 25 | Contains code from lua-multipart-post 26 | 27 | https://github.com/catwell/lua-multipart-post 28 | 29 | Copyright (C) 2012-2013 by Moodstocks SAS 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in 39 | all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 47 | THE SOFTWARE. 48 | -------------------------------------------------------------------------------- /unittest/twitter.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | local OAuth = require "OAuth" 3 | 4 | if _VERSION >= "Lua 5.2" then 5 | _ENV = lunit.module("simple","seeall") 6 | else 7 | module(..., package.seeall, lunit.testcase) 8 | end 9 | 10 | 11 | local consumer_key = "" 12 | local consumer_secret = "" 13 | 14 | --- 15 | -- Teste requesting a token from Twitter and build the authorization url. 16 | -- 17 | function testAuthorize() 18 | local OAuth = require "OAuth" 19 | 20 | local client = OAuth.new(consumer_key, consumer_secret, { 21 | RequestToken = "https://api.twitter.com/oauth/request_token", 22 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 23 | AccessToken = "https://api.twitter.com/oauth/access_token" 24 | }) 25 | 26 | print("requesting token") 27 | local values = client:RequestToken() 28 | assert_table(values) 29 | assert_string(values.oauth_token) 30 | assert_string(values.oauth_token_secret) 31 | --for k,v in pairs(values) do print(k,v) end 32 | 33 | local auth_url = client:BuildAuthorizationUrl() 34 | assert_string(auth_url) 35 | 36 | print("Test authorization at the following URL: " .. auth_url) 37 | end 38 | 39 | 40 | --- 41 | -- Uses the update_with_media Twitter endpoint to test a POST request with multipart/form-data encoding. 42 | -- 43 | function testMultipartPost() 44 | 45 | -- helper 46 | local function read_image() 47 | local f = assert(io.open([[test_twitter_multipart.jpg]], "rb")) 48 | local image_data = f:read("*a") 49 | f:close() 50 | return image_data 51 | end 52 | 53 | local oauth_token = "" 54 | local oauth_token_secret = "" 55 | 56 | local client = OAuth.new(consumer_key, consumer_secret, { 57 | RequestToken = "https://api.twitter.com/oauth/request_token", 58 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 59 | AccessToken = "https://api.twitter.com/oauth/access_token" 60 | }, { 61 | OAuthToken = oauth_token, 62 | OAuthTokenSecret = oauth_token_secret 63 | }) 64 | 65 | local helpers = require "OAuth.helpers" 66 | 67 | local req = helpers.multipart.Request{ 68 | status = "Hello World From Lua!" .. os.time(), 69 | ["media[]"] = { 70 | filename = "@test_multipart.jpg", 71 | data = read_image() 72 | } 73 | } 74 | 75 | local response_code, response_headers, response_status_line, response_body = 76 | client:PerformRequest("POST", "https://api.twitter.com/1.1/statuses/update_with_media.json", req.body, req.headers) 77 | assert_equal(200, response_code) 78 | end 79 | -------------------------------------------------------------------------------- /unittest/termie.lua: -------------------------------------------------------------------------------- 1 | local lunit = require "lunit" 2 | local OAuth = require "OAuth" 3 | 4 | if _VERSION >= "Lua 5.2" then 5 | _ENV = lunit.module("simple","seeall") 6 | else 7 | module(..., package.seeall, lunit.testcase) 8 | end 9 | 10 | 11 | local consumer_key = "key" 12 | local consumer_secret = "secret" 13 | 14 | function test() 15 | print("\nTest: term.ie\n") 16 | local client = OAuth.new(consumer_key, consumer_secret, { 17 | RequestToken = {"http://term.ie/oauth/example/request_token.php", method = "GET"}, 18 | AccessToken = {"http://term.ie/oauth/example/access_token.php", method = "GET"} 19 | }, { 20 | UseAuthHeaders = false 21 | }) 22 | print("Requesting token") 23 | local values, status, headers, response_line, response_body = client:RequestToken() 24 | if not values then 25 | print("Error requesting token:", status) 26 | for k,v in pairs(headers) do print(k,v) end 27 | print(response_line) 28 | print(response_body) 29 | end 30 | assert_table(values) 31 | if not values.oauth_token then 32 | print("Error requesting token:", status) 33 | for k,v in pairs(values) do print(k,v) end 34 | error("No oauth_token") 35 | end 36 | assert_equal("requestkey", values.oauth_token) 37 | assert_equal("requestsecret", values.oauth_token_secret) 38 | 39 | print("Retrieving access token") 40 | local values, status, headers, response_line, response_body = client:GetAccessToken() 41 | if not values then 42 | print("Error requesting token:", status) 43 | for k,v in pairs(headers) do print(k,v) end 44 | print(response_line) 45 | print(response_body) 46 | end 47 | assert_table(values) 48 | if not values.oauth_token then 49 | print("Error requesting token:", status) 50 | for k,v in pairs(values) do print(k,v) end 51 | error("No oauth_token") 52 | end 53 | assert_equal("accesskey", values.oauth_token) 54 | assert_equal("accesssecret", values.oauth_token_secret) 55 | 56 | print("Making authenticated call") 57 | local client = OAuth.new(consumer_key, consumer_secret, { 58 | RequestToken = {"http://term.ie/oauth/example/request_token.php", method = "GET"}, 59 | AccessToken = {"http://term.ie/oauth/example/access_token.php", method = "GET"} 60 | }, { 61 | UseAuthHeaders = false, 62 | OAuthToken = "accesskey", 63 | OAuthTokenSecret = "accesssecret" 64 | }) 65 | local response_code, response_headers, response_status_line, response_body = 66 | client:PerformRequest("GET", "http://term.ie/oauth/example/echo_api.php", { foo="bar"}) 67 | if response_code ~= 200 then 68 | print("Error requesting token:", response_code) 69 | for k,v in pairs(response_headers) do print(k,v) end 70 | print(response_status_line) 71 | print(response_body) 72 | end 73 | print(response_body) 74 | assert_equal("foo=bar", response_body) 75 | end 76 | -------------------------------------------------------------------------------- /src/OAuth/helpers.lua: -------------------------------------------------------------------------------- 1 | local pairs, table, tostring, select = pairs, table, tostring, select 2 | local type, assert, error = type, assert, error 3 | local string = require "string" 4 | local math = require "math" 5 | local Url, Qs 6 | local isLuaNode 7 | 8 | if process then 9 | Url = require "luanode.url" 10 | Qs = require "luanode.querystring" 11 | isLuaNode = true 12 | else 13 | Url = require "socket.url" 14 | end 15 | 16 | local _M = {} 17 | 18 | -- 19 | -- Encodes the key-value pairs of a table according the application/x-www-form-urlencoded content type. 20 | _M.url_encode_arguments = (isLuaNode and Qs.url_encode_arguments) or function(arguments) 21 | local body = {} 22 | for k,v in pairs(arguments) do 23 | body[#body + 1] = Url.escape(tostring(k)) .. "=" .. Url.escape(tostring(v)) 24 | end 25 | return table.concat(body, "&") 26 | end 27 | 28 | 29 | --- 30 | -- Multipart form-data helper. 31 | -- 32 | -- Taken from https://github.com/catwell/lua-multipart-post 33 | -- 34 | do -- Create a scope to avoid these local helpers to escape 35 | 36 | local function fmt(p, ...) 37 | if select('#',...) == 0 then 38 | return p 39 | else 40 | return string.format(p, ...) 41 | end 42 | end 43 | 44 | local function tprintf(t, p, ...) 45 | t[#t + 1] = fmt(p, ...) 46 | end 47 | 48 | local function append_data(r, k, data, extra) 49 | tprintf(r, "content-disposition: form-data; name=\"%s\"", k) 50 | if extra.filename then 51 | tprintf(r, "; filename=\"%s\"", extra.filename) 52 | end 53 | if extra.content_type then 54 | tprintf(r, "\r\ncontent-type: %s", extra.content_type) 55 | end 56 | if extra.content_transfer_encoding then 57 | tprintf(r, "\r\ncontent-transfer-encoding: %s", extra.content_transfer_encoding) 58 | end 59 | tprintf(r, "\r\n\r\n") 60 | tprintf(r, data) 61 | tprintf(r, "\r\n") 62 | end 63 | 64 | local function gen_boundary() 65 | local t = {"BOUNDARY-"} 66 | for i = 2, 17 do 67 | t[i] = string.char(math.random(65, 90)) 68 | end 69 | t[18] = "-BOUNDARY" 70 | return table.concat(t) 71 | end 72 | 73 | local function encode(t, boundary) 74 | local r = {} 75 | local _t, _tkey, key 76 | 77 | -- generate a boundary if none was supplied 78 | boundary = boundary or gen_boundary() 79 | 80 | for k,v in pairs(t) do 81 | tprintf(r,"--%s\r\n",boundary) 82 | 83 | _tkey, _t = type(k), type(v) 84 | 85 | if _tkey ~= "string" and _tkey ~= "number" and _tkey ~= "boolean" then 86 | return nil, ("unexpected type %s for key %s"):format(_tkey, tostring(k)) 87 | end 88 | 89 | if _t == "string" then 90 | append_data(r, k, v, {}) 91 | elseif _t == "number" or _t == "boolean" then 92 | append_data(r, k, tostring(v), {}) 93 | elseif _t == "table" then 94 | assert(v.data, "invalid input") 95 | local extra = { 96 | filename = v.filename or v.name, 97 | content_type = v.content_type or v.mimetype or "application/octet-stream", 98 | content_transfer_encoding = v.content_transfer_encoding or "binary", 99 | } 100 | append_data(r, k, v.data, extra) 101 | else 102 | return nil, ("unexpected type %s for value at key %s"):format(_t, tostring(k)) 103 | end 104 | end 105 | tprintf(r, "--%s--\r\n", boundary) 106 | return table.concat(r) 107 | end 108 | 109 | 110 | _M.multipart = { 111 | --- 112 | -- t is a table with the data to be encoded as multipart/form-data 113 | -- TODO: improve docs 114 | Request = function(t) 115 | local boundary = gen_boundary() 116 | local body, err = encode(t, boundary) 117 | if not body then 118 | return body, err 119 | end 120 | return { 121 | body = body, 122 | headers = { 123 | ["Content-Length"] = #body, 124 | ["Content-Type"] = ("multipart/form-data; boundary=%s"):format(boundary), 125 | }, 126 | } 127 | end 128 | } 129 | 130 | end -- end of multipart scope 131 | 132 | return _M 133 | -------------------------------------------------------------------------------- /src/OAuth/coreLuaSocket.lua: -------------------------------------------------------------------------------- 1 | local Ltn12 = require "ltn12" 2 | local Http = require "socket.http" 3 | local Https = require "ssl.https" 4 | local helpers = require "OAuth.helpers" 5 | 6 | local pairs,type, error, tostring = pairs,type, error, tostring 7 | local table = table 8 | 9 | local _M = {} 10 | 11 | --- 12 | -- Performs the actual http request, using LuaSocket or LuaSec (when using an https scheme) 13 | -- @param url is the url to request 14 | -- @param method is the http method (GET, POST, etc) 15 | -- @param headers are the supplied http headers as a table 16 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 17 | -- or a string (or something that can be converted to a string). In that case, you must supply the Content-Type. 18 | -- @param post_body is a string with all parameters (custom + oauth ones) encoded. This is used when the OAuth provider 19 | -- does not support the 'Authorization' header. 20 | function _M.PerformRequestHelper (self, url, method, headers, arguments, post_body) 21 | -- arguments have already been sanitized 22 | 23 | -- this method screams "refactor me!" 24 | local response_body = {} 25 | local request_constructor = { 26 | url = url, 27 | method = method, 28 | headers = headers, 29 | sink = Ltn12.sink.table(response_body), 30 | redirect = false 31 | } 32 | 33 | if method == "PUT" then 34 | if type(arguments) == "table" then 35 | error("unsupported table argument for PUT") 36 | else 37 | local string_data = tostring(arguments) 38 | if string_data == "nil" then 39 | error("data must be something convertible to a string") 40 | end 41 | request_constructor.headers["Content-Length"] = tostring(#string_data) 42 | request_constructor.source = Ltn12.source.string(string_data) 43 | end 44 | 45 | elseif method == "POST" then 46 | if type(arguments) == "table" then 47 | request_constructor.headers["Content-Type"] = "application/x-www-form-urlencoded" 48 | if not self.m_supportsAuthHeader then 49 | -- send all parameters (oauth + custom) in the body 50 | request_constructor.headers["Content-Length"] = tostring(#post_body) 51 | request_constructor.source = Ltn12.source.string(post_body) 52 | else 53 | -- encode the custom parameters and send them in the body 54 | local source = helpers.url_encode_arguments(arguments) 55 | request_constructor.headers["Content-Length"] = tostring(#source) 56 | request_constructor.source = Ltn12.source.string(source) 57 | end 58 | elseif arguments then 59 | if not self.m_supportsAuthHeader then 60 | error("can't send POST body if the server does not support 'Authorization' header") 61 | end 62 | local string_data = tostring(arguments) 63 | if string_data == "nil" then 64 | error("data must be something convertible to a string") 65 | end 66 | request_constructor.headers["Content-Length"] = tostring(#string_data) 67 | request_constructor.source = Ltn12.source.string(string_data) 68 | else 69 | request_constructor.headers["Content-Length"] = "0" 70 | end 71 | 72 | elseif method == "GET" or method == "HEAD" or method == "DELETE" then 73 | if self.m_supportsAuthHeader then 74 | if arguments then 75 | request_constructor.url = url .. "?" .. helpers.url_encode_arguments(arguments) 76 | end 77 | else 78 | -- send all parameters (oauth + custom) in the url 79 | request_constructor.url = url .. "?" .. post_body 80 | end 81 | end 82 | 83 | local ok, response_code, response_headers, response_status_line 84 | if url:match("^https://") then 85 | ok, response_code, response_headers, response_status_line = Https.request(request_constructor) 86 | elseif url:match("^http://") then 87 | ok, response_code, response_headers, response_status_line = Http.request(request_constructor) 88 | else 89 | error( ("unsupported scheme '%s'"):format( tostring(url:match("^([^:]+)")) ) ) 90 | end 91 | 92 | if not ok then 93 | return nil, response_code, response_headers, response_status_line, response_body 94 | end 95 | 96 | response_body = table.concat(response_body) 97 | 98 | --[=[ 99 | for k,v in pairs(response_headers or {}) do 100 | print( ("%s: %s"):format(k,v) ) 101 | end 102 | print( ("response: %s"):format(response_body) ) 103 | --]=] 104 | 105 | return true, response_code, response_headers, response_status_line, response_body 106 | end 107 | 108 | return _M 109 | -------------------------------------------------------------------------------- /src/OAuth/coreLuaNode.lua: -------------------------------------------------------------------------------- 1 | local Http = require "luanode.http" 2 | local Url = require "luanode.url" 3 | 4 | local helpers = require "OAuth.helpers" 5 | local console = require "luanode.console" 6 | 7 | local pairs,type, error, tostring = pairs,type, error, tostring 8 | local table = table 9 | 10 | local _M = {} 11 | 12 | --- 13 | -- Performs the actual http request, using LuaSocket or LuaSec (when using an https scheme) 14 | -- @param url is the url to request 15 | -- @param method is the http method (GET, POST, etc) 16 | -- @param headers are the supplied http headers as a table 17 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 18 | -- or a string (or something that can be converted to a string). In that case, you must supply the Content-Type. 19 | -- @param post_body is a string with all parameters (custom + oauth ones) encoded. This is used when the OAuth provider 20 | -- does not support the 'Authorization' header. 21 | -- @param callback is a function to be called with the results of the request when they're available. The callback 22 | -- receives the following arguments: an (optional) error object, http status code, http response headers, 23 | -- http status line and the response body 24 | -- In case of a connection error (host unreacheable, etc), the callback will be called with 25 | -- { message = , 26 | -- code = 27 | -- } 28 | -- 29 | function _M.PerformRequestHelper (self, url, method, headers, arguments, post_body, callback) 30 | -- arguments have already been sanitized 31 | 32 | -- this method screams "refactor me!" 33 | local parsedUrl = Url.parse(url) 34 | 35 | local request_constructor = { 36 | url = url, 37 | method = method, 38 | headers = headers, 39 | } 40 | 41 | if method == "PUT" then 42 | if type(arguments) == "table" then 43 | error("unsupported table argument for PUT") 44 | else 45 | local string_data = tostring(arguments) 46 | if string_data == "nil" then 47 | error("data must be something convertible to a string") 48 | end 49 | request_constructor.headers["Content-Length"] = tostring(#string_data) 50 | request_constructor.source = string_data 51 | end 52 | 53 | elseif method == "POST" then 54 | if type(arguments) == "table" then 55 | request_constructor.headers["Content-Type"] = "application/x-www-form-urlencoded" 56 | if not self.m_supportsAuthHeader then 57 | -- send all parameters (oauth + custom) in the body 58 | request_constructor.headers["Content-Length"] = tostring(#post_body) 59 | request_constructor.source = post_body 60 | else 61 | -- encode the custom parameters and send them in the body 62 | local source = helpers.url_encode_arguments(arguments) 63 | request_constructor.headers["Content-Length"] = tostring(#source) 64 | request_constructor.source = source 65 | end 66 | elseif arguments then 67 | if not self.m_supportsAuthHeader then 68 | error("can't send POST body if the server does not support 'Authorization' header") 69 | end 70 | local string_data = tostring(arguments) 71 | if string_data == "nil" then 72 | error("data must be something convertible to a string") 73 | end 74 | request_constructor.headers["Content-Length"] = tostring(#string_data) 75 | request_constructor.source = string_data 76 | else 77 | request_constructor.headers["Content-Length"] = "0" 78 | end 79 | 80 | elseif method == "GET" or method == "HEAD" or method == "DELETE" then 81 | if self.m_supportsAuthHeader then 82 | if arguments then 83 | request_constructor.url = parsedUrl.path .. "?" .. helpers.url_encode_arguments(arguments) 84 | end 85 | else 86 | -- send all parameters (oauth + custom) in the url 87 | request_constructor.url = parsedUrl.path .. "?" .. post_body 88 | end 89 | end 90 | 91 | local secure = false 92 | 93 | if parsedUrl.protocol == "https:" then 94 | secure = true 95 | parsedUrl.port = parsedUrl.port or 443 96 | elseif parsedUrl.protocol == "http:" then 97 | secure = false 98 | parsedUrl.port = parsedUrl.port or 80 99 | else 100 | error( ("unsupported scheme '%s'"):format( parsedUrl.protocol ) ) 101 | end 102 | 103 | local client = Http.createClient(parsedUrl.port, parsedUrl.host, secure) 104 | 105 | client:on("error", function(self, err_msg, err_code) 106 | callback({ code = err_code, message = err_msg }) 107 | end) 108 | 109 | -- this sucks! 110 | request_constructor.headers["Host"] = parsedUrl.host 111 | 112 | local request = client:request(method, request_constructor.url, request_constructor.headers) 113 | 114 | request.__buffer = {} 115 | 116 | request:on("response", function(self, response) 117 | 118 | response:on("data", function(self, data) 119 | request.__buffer[#request.__buffer + 1] = data 120 | end) 121 | 122 | response:on("end", function(self) 123 | callback(nil, response.statusCode, response.headers, tostring(response.statusCode), table.concat(request.__buffer)) 124 | end) 125 | end) 126 | 127 | if request_constructor.source then 128 | request:finish(request_constructor.source) 129 | else 130 | request:finish() 131 | end 132 | 133 | 134 | return true 135 | end 136 | 137 | return _M 138 | -------------------------------------------------------------------------------- /src/OAuth2.lua: -------------------------------------------------------------------------------- 1 | local Http = require "socket.http" 2 | --local Https = require "ssl.https" 3 | --local Https = require "https" 4 | require "https" 5 | local Https = ssl.https 6 | local Base64 = require "base64" 7 | local Ltn12 = require "ltn12" 8 | local Url = require "socket.url" 9 | --local Crypto = require "crypto" 10 | 11 | 12 | local Client = {} 13 | Client.__index = Client 14 | 15 | -- 16 | -- Encodes the key-value pairs of a table according the application/x-www-form-urlencoded content type. 17 | local function url_encode_arguments(arguments) 18 | local body = {} 19 | for k,v in pairs(arguments) do 20 | body[#body + 1] = Url.escape(tostring(k)) .. "=" .. Url.escape(tostring(v)) 21 | end 22 | return table.concat(body, "&") 23 | end 24 | 25 | --- 26 | -- Performs the actual http request, using LuaSocket or LuaSec (when using an https scheme) 27 | -- @param url is the url to request 28 | -- @param method is the http method (GET, POST, etc) 29 | -- @param headers are the supplied http headers as a table 30 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 31 | -- or a string (or something that can be converted to a string). In that case, you must supply the Content-Type. 32 | -- @param post_body is a string with all parameters (custom + oauth ones) encoded. This is used when the OAuth provider 33 | -- does not support the 'Authorization' header. 34 | local function PerformRequestHelper(self, url, method, headers, arguments, post_body) 35 | -- Remove oauth_related arguments 36 | if type(arguments) == "table" then 37 | for k,v in pairs(arguments) do 38 | if type(k) == "string" and k:match("^oauth_") then 39 | arguments[k] = nil 40 | end 41 | end 42 | if not next(arguments) then 43 | arguments = nil 44 | end 45 | end 46 | 47 | -- this method screams "refactor me!" 48 | local response_body = {} 49 | local request_constructor = { 50 | url = url, 51 | method = method, 52 | headers = headers, 53 | sink = Ltn12.sink.table(response_body) 54 | } 55 | 56 | if method == "PUT" then 57 | if type(arguments) == "table" then 58 | error("unsupported table argument for PUT") 59 | else 60 | local string_data = tostring(arguments) 61 | if string_data == "nil" then 62 | error("data must be something convertible to a string") 63 | end 64 | request_constructor.headers["Content-Length"] = tostring(#string_data) 65 | request_constructor.source = Ltn12.source.string(string_data) 66 | end 67 | 68 | elseif method == "POST" then 69 | if type(arguments) == "table" then 70 | request_constructor.headers["Content-Type"] = "application/x-www-form-urlencoded" 71 | if not self.m_supportsAuthHeader then 72 | -- send all parameters (oauth + custom) in the body 73 | request_constructor.headers["Content-Length"] = tostring(#post_body) 74 | request_constructor.source = Ltn12.source.string(post_body) 75 | else 76 | -- encode the custom parameters and send them in the body 77 | local source = url_encode_arguments(arguments) 78 | request_constructor.headers["Content-Length"] = tostring(#source) 79 | request_constructor.source = Ltn12.source.string(source) 80 | end 81 | elseif arguments then 82 | if not self.m_supportsAuthHeader then 83 | error("can't send POST body if the server does not support 'Authorization' header") 84 | end 85 | local string_data = tostring(arguments) 86 | if string_data == "nil" then 87 | error("data must be something convertible to a string") 88 | end 89 | request_constructor.headers["Content-Length"] = tostring(#string_data) 90 | request_constructor.source = Ltn12.source.string(string_data) 91 | else 92 | request_constructor.headers["Content-Length"] = "0" 93 | end 94 | 95 | elseif method == "GET" or method == "HEAD" or method == "DELETE" then 96 | if self.m_supportsAuthHeader then 97 | if arguments then 98 | request_constructor.url = url .. "?" .. url_encode_arguments(arguments) 99 | end 100 | else 101 | -- send all parameters (oauth + custom) in the url 102 | request_constructor.url = url .. "?" .. post_body 103 | end 104 | end 105 | 106 | local ok, response_code, response_headers, response_status_line 107 | if url:match("^https://") then 108 | ok, response_code, response_headers, response_status_line = Https.request(request_constructor) 109 | elseif url:match("^http://") then 110 | ok, response_code, response_headers, response_status_line = Http.request(request_constructor) 111 | else 112 | error( ("unsupported scheme '%s'"):format( tostring(url:match("^([^:]+)")) ) ) 113 | end 114 | 115 | if not ok then 116 | return nil, response_code, response_headers, response_status_line, response_body 117 | end 118 | 119 | response_body = table.concat(response_body) 120 | 121 | --[=[ 122 | for k,v in pairs(response_headers or {}) do 123 | print( ("%s: %s"):format(k,v) ) 124 | end 125 | print( ("response: %s"):format(response_body) ) 126 | --]=] 127 | 128 | return true, response_code, response_headers, response_status_line, response_body 129 | end 130 | 131 | 132 | 133 | -- 134 | -- After retrieving an access token, this method is used to issue properly authenticated requests. 135 | -- (see http://tools.ietf.org/html/rfc5849#section-3) 136 | -- @param method is the http method (GET, POST, etc) 137 | -- @param url is the url to request 138 | -- @param arguments is an optional table whose keys and values will be encoded as "application/x-www-form-urlencoded" 139 | -- (when doing a POST) or encoded and sent in the query string (when doing a GET). 140 | -- @param headers is an optional table with http headers to be sent in the request 141 | -- @return the http status code (a number), a table with the response headers, the status line and the response itself 142 | -- 143 | function Client:PerformRequest(method, url, arguments, headers) 144 | assert(type(method) == "string", "'method' must be a string") 145 | method = method:upper() 146 | 147 | --local headers, post_body, arguments = self:BuildRequest(method, url, arguments, headers) 148 | arguments = arguments or {} 149 | arguments.client_id = self.m_consumer_key 150 | local ok, response_code, response_headers, response_status_line, response_body = PerformRequestHelper(self, url, method, headers, arguments, post_body) 151 | return response_code, response_headers, response_status_line, response_body 152 | end 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | function Client.new(consumer_key, consumer_secret, endpoints, params) 163 | params = params or {} 164 | local newInstance = { 165 | m_consumer_key = consumer_key, 166 | m_consumer_secret = consumer_secret, 167 | m_endpoints = {}, 168 | m_signature_method = params.SignatureMethod or "HMAC-SHA1", 169 | m_supportsAuthHeader = true, 170 | m_oauth_token = params.OAuthToken, 171 | m_oauth_token_secret = params.OAuthTokenSecret, 172 | m_oauth_verifier = params.OAuthVerifier 173 | } 174 | 175 | if type(params.UseAuthHeaders) == "boolean" then 176 | newInstance.m_supportsAuthHeader = params.UseAuthHeaders 177 | end 178 | 179 | for k,v in pairs(endpoints or {}) do 180 | if type(v) == "table" then 181 | newInstance.m_endpoints[k] = { url = v[1], method = string.upper(v.method) } 182 | else 183 | newInstance.m_endpoints[k] = { url = v, method = "POST" } 184 | end 185 | end 186 | 187 | setmetatable(newInstance, Client) 188 | 189 | return newInstance 190 | end 191 | 192 | return Client 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua OAuth # 2 | 3 | [![Build Status](https://travis-ci.org/ignacio/LuaOAuth.png?branch=master)](https://travis-ci.org/ignacio/LuaOAuth) 4 | [![Coverage Status](https://coveralls.io/repos/github/ignacio/LuaOAuth/badge.svg?branch=master)](https://coveralls.io/github/ignacio/LuaOAuth?branch=master) 5 | 6 | A Lua client library for OAuth 1.0 enabled servers. 7 | 8 | This is an adaptation of Jeffrey Friedl's [Twitter OAuth Authentication Routines in Lua, for Lightroom Plugins][1], 9 | with Lightroom's code replaced by other libraries (i.e. [LuaSec][2], [LuaSocket][3], etc) and with HMAC-SHA1 calculations 10 | done with [LuaCrypto][4] instead of [plain Lua][6]. 11 | 12 | Most of the code was taken from [Jeffrey Friedl's blog][1]. Multipart encoding was adapted from [lua-multipart-post][9]. 13 | 14 | LuaOAuth supports two modes of operation. A "synchronous" mode were you block while you wait for the results or an 15 | "asynchronous" mode where you must supply "callbacks" in order to receive the results. LuaOAuth will behave asynchronously 16 | when used in conjunction with [LuaNode][8]. 17 | 18 | It is tested to work with [Lua 5.1][11], [Lua 5.2][9], [LuaJIT 2.0.4][10], [LuaJIT 2.1 (development)][10] and [LuaNode][8]. 19 | 20 | ## Usage # 21 | 22 | There is some documentation available [on the wiki](https://github.com/ignacio/LuaOAuth/wiki/API-Documentation) and also 23 | you can take a look at the unit tests. For instance, the file ''unittest/echo_lab_madgex_com.lua'' creates a client that 24 | uses the test service provided by [madgex.com][5]. 25 | 26 | Basically, you create an OAuth client with your consumer key and secret, providing the OAuth service's endpoints URLs (i.e. 27 | where to request a token, where to get an access token, etc). 28 | 29 | ``` lua 30 | local OAuth = require "OAuth" 31 | local client = OAuth.new("key", "secret", { 32 | RequestToken = "http://echo.lab.madgex.com/request-token.ashx", 33 | AccessToken = "http://echo.lab.madgex.com/access-token.ashx" 34 | }) 35 | ``` 36 | 37 | Now you can request a token and then an access token: 38 | 39 | ``` lua 40 | local values = client:RequestToken() 41 | values = client:GetAccessToken() 42 | ``` 43 | 44 | Once you have been authorized, you can perform requests: 45 | 46 | ``` lua 47 | local code, headers, statusLine, body = client:PerformRequest("POST", "http://echo.lab.madgex.com/echo.ashx", {status = "Hello World From Lua (again)!" .. os.time()}) 48 | ``` 49 | 50 | That is called the "synchronous" api. But if you use LuaOAuth with [LuaNode][8] you'll need to use the "asynchronous" api: 51 | 52 | ``` lua 53 | local OAuth = require "OAuth" 54 | local client = OAuth.new("key", "secret", { 55 | RequestToken = "http://echo.lab.madgex.com/request-token.ashx", 56 | AccessToken = "http://echo.lab.madgex.com/access-token.ashx" 57 | }) 58 | client:RequestToken(function(values) 59 | -- I receive the token in the callback I've supplied. 60 | end) 61 | ``` 62 | 63 | ## A more involved example # 64 | This example will show how to use LuaOAuth with Twitter. It assumes that you have already created a Twitter application. 65 | If you didn't, go to [Twitter's developer site][7] and create a new one. 66 | 67 | For instance, let's assume that you've created the application "MyTestApp", your consumer key is 'consumer_key' and your 68 | consumer secret is 'consumer_secret'. Your Twitter user is 'johncleese'. 69 | 70 | Now, you need to authorize the application. Let's use the "Out of band" (OOB) method. With this method, we will: 71 | 72 | - get a "Request Token" so we can build an authorization url. 73 | - then we'll navigate with our browser to that url and we'll enter our Twitter username and password (if we weren't logged in yet). 74 | - then we'll authorize the application. A PIN will appear on the screen. 75 | - with that PIN, we'll complete the process so we get an "Access Token". 76 | 77 | Copy the following in a script and run it from the console. Follow the instructions. 78 | 79 | ``` lua 80 | local OAuth = require "OAuth" 81 | local client = OAuth.new("consumer_key", "consumer_secret", { 82 | RequestToken = "https://api.twitter.com/oauth/request_token", 83 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 84 | AccessToken = "https://api.twitter.com/oauth/access_token" 85 | }) 86 | local callback_url = "oob" 87 | local values = client:RequestToken({ oauth_callback = callback_url }) 88 | local oauth_token = values.oauth_token -- we'll need both later 89 | local oauth_token_secret = values.oauth_token_secret 90 | 91 | local tracking_code = "90210" -- this is some random value 92 | local new_url = client:BuildAuthorizationUrl({ oauth_callback = callback_url, state = tracking_code }) 93 | 94 | print("Navigate to this url with your browser, please...") 95 | print(new_url) 96 | print("\r\nOnce you have logged in and authorized the application, enter the PIN") 97 | 98 | local oauth_verifier = assert(io.read("*n")) -- read the PIN from stdin 99 | oauth_verifier = tostring(oauth_verifier) -- must be a string 100 | 101 | -- now we'll use the tokens we got in the RequestToken call, plus our PIN 102 | local client = OAuth.new("consumer_key", "consumer_secret", { 103 | RequestToken = "https://api.twitter.com/oauth/request_token", 104 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 105 | AccessToken = "https://api.twitter.com/oauth/access_token" 106 | }, { 107 | OAuthToken = oauth_token, 108 | OAuthVerifier = oauth_verifier 109 | }) 110 | client:SetTokenSecret(oauth_token_secret) 111 | 112 | local values, err, headers, status, body = client:GetAccessToken() 113 | for k, v in pairs(values) do 114 | print(k,v) 115 | end 116 | ``` 117 | 118 | If everything went well, something like this will be printed: 119 | 120 | screen_name johncleese (this will be your username) 121 | oauth_token dasklhdnmpunexoibrunkljdsflkj191919409 122 | oauth_token_secret AIUSUMAOoq983092874bibiuwewqlknjSUXt 123 | user_id 99999999 124 | 125 | Store 'oauth_token' and 'oauth_token_secret' somewhere, we'll need them later. 126 | 127 | We are now able to issue some requests. So type the following in a script. Remember that the values of 'oauth_token' and 128 | 'oauth_token_secret' needs to be replaced with what you got in the last step. 129 | 130 | ``` lua 131 | local oauth_token = "" 132 | local oauth_token_secret = "" 133 | 134 | local client = OAuth.new("consumer_key", "consumer_secret", { 135 | RequestToken = "https://api.twitter.com/oauth/request_token", 136 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 137 | AccessToken = "https://api.twitter.com/oauth/access_token" 138 | }, { 139 | OAuthToken = oauth_token, 140 | OAuthTokenSecret = oauth_token_secret 141 | }) 142 | 143 | -- the mandatory "Hello World" example... 144 | local response_code, response_headers, response_status_line, response_body = 145 | client:PerformRequest("POST", "https://api.twitter.com/1.1/statuses/update.json", {status = "Hello World From Lua!" .. os.time()}) 146 | print("response_code", response_code) 147 | print("response_status_line", response_status_line) 148 | for k,v in pairs(response_headers) do print(k,v) end 149 | print("response_body", response_body) 150 | ``` 151 | 152 | Now, let's try to request my Tweets. 153 | 154 | ``` lua 155 | local oauth_token = "" 156 | local oauth_token_secret = "" 157 | 158 | local client = OAuth.new("consumer_key", "consumer_secret", { 159 | RequestToken = "https://api.twitter.com/oauth/request_token", 160 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 161 | AccessToken = "https://api.twitter.com/oauth/access_token" 162 | }, { 163 | OAuthToken = oauth_token, 164 | OAuthTokenSecret = oauth_token_secret 165 | }) 166 | 167 | -- the mandatory "Hello World" example... 168 | local response_code, response_headers, response_status_line, response_body = 169 | client:PerformRequest("GET", "https://api.twitter.com/1.1/statuses/user_timeline.json", {screen_name = "iburgueno"}) 170 | print("response_code", response_code) 171 | print("response_status_line", response_status_line) 172 | for k,v in pairs(response_headers) do print(k,v) end 173 | print("response_body", response_body) 174 | ``` 175 | 176 | And now, let's post an image: 177 | 178 | ``` lua 179 | local oauth_token = "" 180 | local oauth_token_secret = "" 181 | 182 | local client = OAuth.new("consumer_key", "consumer_secret", { 183 | RequestToken = "https://api.twitter.com/oauth/request_token", 184 | AuthorizeUser = {"https://api.twitter.com/oauth/authorize", method = "GET"}, 185 | AccessToken = "https://api.twitter.com/oauth/access_token" 186 | }, { 187 | OAuthToken = oauth_token, 188 | OAuthTokenSecret = oauth_token_secret 189 | }) 190 | 191 | local helpers = require "OAuth.helpers" 192 | 193 | local req = helpers.multipart.Request{ 194 | status = "Hello World From Lua!", 195 | ["media[]"] = { 196 | filename = "@picture.jpg", 197 | data = picture_data -- some picture file you have read 198 | } 199 | } 200 | local response_code, response_headers, response_status_line, response_body = 201 | client:PerformRequest("POST", 202 | "https://api.twitter.com/1.1/statuses/update_with_media.json", 203 | req.body, req.headers 204 | ) 205 | print("response_code", response_code) 206 | print("response_status_line", response_status_line) 207 | for k,v in pairs(response_headers) do print(k,v) end 208 | print("response_body", response_body) 209 | ``` 210 | 211 | 212 | [1]: http://regex.info/blog/lua/twitter 213 | [2]: http://www.inf.puc-rio.br/~brunoos/luasec/ 214 | [3]: http://w3.impa.br/~diego/software/luasocket/ 215 | [4]: http://luacrypto.luaforge.net/ 216 | [5]: http://echo.lab.madgex.com/ 217 | [6]: http://regex.info/blog/lua/sha1 218 | [7]: http://dev.twitter.com/apps 219 | [8]: https://github.com/ignacio/luanode 220 | [9]: https://github.com/catwell/lua-multipart-post 221 | [10]: http://luajit.org/ 222 | [11]: http://www.lua.org/ 223 | -------------------------------------------------------------------------------- /src/OAuth.lua: -------------------------------------------------------------------------------- 1 | local Base64 = require "base64" 2 | local Crypto 3 | local core 4 | local isLuaNode 5 | 6 | if process then 7 | Crypto = require "luanode.crypto" 8 | core = require "OAuth.coreLuaNode" 9 | isLuaNode = true 10 | else 11 | Crypto = require "crypto" 12 | core = require "OAuth.coreLuaSocket" 13 | isLuaNode = false 14 | end 15 | 16 | 17 | local table, string, os, print = table, string, os, print 18 | local error, assert = error, assert 19 | local pairs, tostring, type, next, setmetatable = pairs, tostring, type, next, setmetatable 20 | local tonumber = tonumber 21 | local math = math 22 | 23 | 24 | local Client = {} 25 | Client.__index = Client 26 | 27 | local m_valid_http_methods = { 28 | GET = true, 29 | HEAD = true, 30 | POST = true, 31 | PUT = true, 32 | DELETE = true 33 | } 34 | 35 | -- each instance has this fields 36 | -- m_supportsAuthHeader 37 | -- m_consumer_secret 38 | -- m_signature_method 39 | -- m_oauth_verifier 40 | -- m_endpoints 41 | -- m_oauth_token 42 | -- m_oauth_token_secret 43 | 44 | 45 | -- 46 | -- Joins table t1 and t2 47 | local function merge(t1, t2) 48 | assert(t1) 49 | if not t2 then return t1 end 50 | for k,v in pairs(t2) do 51 | t1[k] = v 52 | end 53 | return t1 54 | end 55 | 56 | -- 57 | -- Generates a unix timestamp (epoch is 1970, etc) 58 | local function generate_timestamp() 59 | return tostring(os.time()) 60 | end 61 | 62 | -- 63 | -- Generates a nonce (number used once). 64 | -- I'm not base64 encoding the resulting nonce because some providers rejects them (i.e. echo.lab.madgex.com) 65 | local function generate_nonce() 66 | local nonce = tostring(math.random()) .. "random" .. tostring(os.time()) 67 | 68 | if isLuaNode then 69 | return Crypto.createHmac("sha1", "keyyyy"):update(nonce):final('hex') 70 | else 71 | return Crypto.hmac.digest("sha1", nonce, "keyyyy") 72 | end 73 | end 74 | 75 | -- 76 | -- Like URL-encoding, but following OAuth's specific semantics 77 | local function oauth_encode(val) 78 | return val:gsub('[^-._~a-zA-Z0-9]', function(letter) 79 | return string.format("%%%02x", letter:byte()):upper() 80 | end) 81 | end 82 | 83 | --- 84 | -- Taken from LuaSocket (socket.url) 85 | -- 86 | local function url_unescape(s) 87 | return (string.gsub(s, "%%(%x%x)", function(hex) 88 | return string.char(tonumber(hex, 16)) 89 | end)) 90 | end 91 | 92 | -- 93 | -- Given a url endpoint, a valid Http method, and a table of key/value args, build the query string and sign it, 94 | -- returning the oauth_signature, the query string and the Authorization header (if supported) 95 | -- 96 | -- The args should also contain an 'oauth_token_secret' item, except for the initial token request. 97 | -- See: http://dev.twitter.com/pages/auth#signing-requests 98 | -- 99 | function Client:Sign(httpMethod, baseUri, arguments, oauth_token_secret, authRealm) 100 | assert(m_valid_http_methods[httpMethod], "method '" .. httpMethod .. "' not supported") 101 | 102 | local consumer_secret = self.m_consumer_secret 103 | local token_secret = oauth_token_secret or "" 104 | 105 | -- oauth-encode each key and value, and get them set up for a Lua table sort. 106 | local keys_and_values = { } 107 | 108 | --print("'"..consumer_secret.."'") 109 | --print("'"..token_secret.."'") 110 | for key, val in pairs(arguments) do 111 | table.insert(keys_and_values, { 112 | key = oauth_encode(key), 113 | val = oauth_encode(tostring(val)) 114 | }) 115 | end 116 | 117 | -- Sort by key first, then value 118 | table.sort(keys_and_values, function(a,b) 119 | if a.key < b.key then 120 | return true 121 | elseif a.key > b.key then 122 | return false 123 | else 124 | return a.val < b.val 125 | end 126 | end) 127 | 128 | -- Now combine key and value into key=value 129 | local key_value_pairs = { } 130 | for _, rec in pairs(keys_and_values) do 131 | --print("'"..rec.key.."'", "'"..rec.val.."'") 132 | table.insert(key_value_pairs, rec.key .. "=" .. rec.val) 133 | end 134 | 135 | -- Now we have the query string we use for signing, and, after we add the signature, for the final as well. 136 | local query_string_except_signature = table.concat(key_value_pairs, "&") 137 | 138 | -- Don't need it for Twitter, but if this routine is ever adapted for 139 | -- general OAuth signing, we may need to massage a version of the url to 140 | -- remove query elements, as described in http://oauth.net/core/1.0a#rfc.section.9.1.2 141 | -- 142 | -- More on signing: 143 | -- http://www.hueniverse.com/hueniverse/2008/10/beginners-gui-1.html 144 | -- 145 | local signature_base_string = httpMethod .. '&' .. oauth_encode(baseUri) .. '&' .. oauth_encode(query_string_except_signature) 146 | --print( ("Signature base string: %s":format(signature_base_string) ) 147 | local signature_key = oauth_encode(consumer_secret) .. '&' .. oauth_encode(token_secret) 148 | --print( ("Signature key: %s"):format(signature_key) ) 149 | 150 | -- Now have our text and key for HMAC-SHA1 signing 151 | local hmac_binary 152 | if isLuaNode then 153 | hmac_binary = Crypto.createHmac("sha1", signature_key):update(signature_base_string):final("binary") 154 | else 155 | hmac_binary = Crypto.hmac.digest("sha1", signature_base_string, signature_key, true) 156 | end 157 | 158 | -- Base64 encode it 159 | local hmac_b64 = Base64.encode(hmac_binary) 160 | 161 | local oauth_signature = oauth_encode(hmac_b64) 162 | 163 | local oauth_headers 164 | -- Build the 'Authorization' header if the provider supports it 165 | if self.m_supportsAuthHeader then 166 | oauth_headers = { ([[OAuth realm="%s"]]):format(authRealm or "") } 167 | for k,v in pairs(arguments) do 168 | if k:match("^oauth_") then 169 | table.insert(oauth_headers, k .. "=\"" .. oauth_encode(v) .. "\"") 170 | end 171 | end 172 | table.insert(oauth_headers, "oauth_signature=\"" .. oauth_signature .. "\"") 173 | oauth_headers = table.concat(oauth_headers, ", ") 174 | end 175 | 176 | return oauth_signature, query_string_except_signature .. '&oauth_signature=' .. oauth_signature, oauth_headers 177 | end 178 | 179 | --- 180 | -- Performs the actual http request, using LuaSocket or LuaSec (when using an https scheme) 181 | -- @param url is the url to request 182 | -- @param method is the http method (GET, POST, etc) 183 | -- @param headers are the supplied http headers as a table 184 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 185 | -- or a string (or something that can be converted to a string). In that case, you must supply the Content-Type. 186 | -- @param post_body is a string with all parameters (custom + oauth ones) encoded. This is used when the OAuth provider 187 | -- does not support the 'Authorization' header. 188 | -- @param callback is only required if running under LuaNode. It is a function to be called when the response is available. 189 | -- 190 | local function PerformRequestHelper (self, url, method, headers, arguments, post_body, callback) 191 | -- Remove oauth_related arguments 192 | if type(arguments) == "table" then 193 | for k,v in pairs(arguments) do 194 | if type(k) == "string" and k:match("^oauth_") then 195 | arguments[k] = nil 196 | end 197 | end 198 | if not next(arguments) then 199 | arguments = nil 200 | end 201 | end 202 | 203 | return core.PerformRequestHelper(self, url, method, headers, arguments, post_body, callback) 204 | end 205 | 206 | 207 | 208 | --- 209 | -- Requests an Unauthorized Request Token (http://tools.ietf.org/html/rfc5849#section-2.1) 210 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 211 | -- (when doing a POST) or encoded and sent in the query string (when doing a GET). 212 | -- @param headers is an optional table with http headers to be sent in the request 213 | -- @param callback is only required if running under LuaNode. It is a function to be called with a table with the 214 | -- obtained token or [false, http status code, http response headers, http status line and the response body] in case 215 | -- of an error. The callback is mandatory when running under LuaNode. 216 | -- @return nothing if running under LuaNode (the callback will be called instead). Else it will return a 217 | -- table containing the returned values from the server if succesfull or throws an error otherwise. 218 | -- 219 | function Client:RequestToken(arguments, headers, callback) 220 | 221 | if type(arguments) == "function" then 222 | callback = arguments 223 | arguments, headers = nil, nil 224 | elseif type(headers) == "function" then 225 | callback = headers 226 | headers = nil 227 | end 228 | 229 | if isLuaNode and not callback then 230 | error("RequestToken needs a callback") 231 | end 232 | 233 | local args = { 234 | oauth_consumer_key = self.m_consumer_key, 235 | oauth_nonce = generate_nonce(), 236 | oauth_signature_method = self.m_signature_method, 237 | oauth_timestamp = generate_timestamp(), 238 | oauth_version = "1.0" -- optional mi trasero! 239 | } 240 | args = merge(args, arguments) 241 | 242 | local endpoint = self.m_endpoints.RequestToken 243 | 244 | local oauth_signature, post_body, authHeader = self:Sign(endpoint.method, endpoint.url, args) 245 | 246 | local headers = merge({}, headers) 247 | if self.m_supportsAuthHeader then 248 | headers["Authorization"] = authHeader 249 | end 250 | 251 | if not callback then 252 | local ok, response_code, response_headers, response_status_line, response_body = PerformRequestHelper(self, endpoint.url, endpoint.method, headers, arguments, post_body) 253 | --print(ok, response_code, response_headers, response_status_line, response_body) 254 | if not ok or response_code ~= 200 then 255 | -- can't do much, the responses are not standard 256 | return nil, response_code, response_headers, response_status_line, response_body 257 | end 258 | local values = {} 259 | for key, value in string.gmatch(response_body, "([^&=]+)=([^&=]*)&?" ) do 260 | --print( ("key=%s, value=%s"):format(key, value) ) 261 | -- The response parameters are url-encodeded per RFC 5849 so we need to decode them 262 | values[key] = url_unescape(value) 263 | end 264 | 265 | self.m_oauth_token_secret = values.oauth_token_secret 266 | self.m_oauth_token = values.oauth_token 267 | 268 | return values 269 | 270 | else 271 | 272 | local oauth_instance = self 273 | 274 | PerformRequestHelper(self, endpoint.url, endpoint.method, headers, arguments, post_body, 275 | function(err, response_code, response_headers, response_status_line, response_body) 276 | if err then 277 | callback(err) 278 | return 279 | end 280 | if response_code ~= 200 then 281 | -- can't do much, the responses are not standard 282 | callback({ status = response_code, headers = response_headers, status_line = response_status_line, body = response_body}) 283 | return 284 | end 285 | local values = {} 286 | for key, value in string.gmatch(response_body, "([^&=]+)=([^&=]*)&?" ) do 287 | --print( ("key=%s, value=%s"):format(key, value) ) 288 | values[key] = url_unescape(value) 289 | end 290 | 291 | oauth_instance.m_oauth_token_secret = values.oauth_token_secret 292 | oauth_instance.m_oauth_token = values.oauth_token 293 | 294 | callback(nil, values) 295 | end) 296 | end 297 | end 298 | 299 | --- 300 | -- Requests Authorization from the User (http://tools.ietf.org/html/rfc5849#section-2.2) 301 | -- Builds the URL used to issue a request to the Service Provider's User Authorization URL 302 | -- @param arguments is an optional table whose keys and values will be encoded and sent in the query string. 303 | -- @return the fully constructed URL, with oauth_token and custom parameters encoded. 304 | -- 305 | function Client:BuildAuthorizationUrl(arguments) 306 | local args = { } 307 | args = merge(args, arguments) 308 | args.oauth_token = (arguments and arguments.oauth_token) or self.m_oauth_token or error("no oauth_token") 309 | 310 | -- oauth-encode each key and value 311 | local keys_and_values = { } 312 | for key, val in pairs(args) do 313 | table.insert(keys_and_values, { 314 | key = oauth_encode(key), 315 | val = oauth_encode(tostring(val)) 316 | }) 317 | end 318 | 319 | -- Now combine key and value into key=value 320 | local key_value_pairs = { } 321 | for _, rec in pairs(keys_and_values) do 322 | table.insert(key_value_pairs, rec.key .. "=" .. rec.val) 323 | end 324 | local query_string = table.concat(key_value_pairs, "&") 325 | 326 | local endpoint = self.m_endpoints.AuthorizeUser 327 | return endpoint.url .. "?" .. query_string 328 | end 329 | 330 | --[=[ This seems to be unnecesary 331 | -- 332 | -- Requests Authorization from the User (6.2) http://oauth.net/core/1.0a/#auth_step2 333 | -- Builds and issues the request 334 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 335 | -- (when doing a POST) or encoded and sent in the query string (when doing a GET). 336 | -- @param headers is an optional table with http headers to be sent in the request 337 | -- @return the http status code (a number), a table with the response headers and the response itself 338 | function Authorize(self, arguments, headers) 339 | local args = { 340 | oauth_consumer_key = self.m_consumer_key, 341 | oauth_nonce = generate_nonce(), 342 | oauth_signature_method = self.m_signature_method, 343 | oauth_timestamp = generate_timestamp(), 344 | oauth_version = "1.0" 345 | } 346 | args = merge(args, arguments) 347 | args.oauth_token = (arguments and arguments.oauth_token) or self.m_oauth_token or error("no oauth_token") 348 | 349 | local endpoint = self.m_endpoints.AuthorizeUser 350 | 351 | local oauth_signature, post_body, authHeader = self:Sign(endpoint.method, endpoint.url, args) 352 | 353 | local headers = merge({}, headers) 354 | if self.m_supportsAuthHeader then 355 | headers["Authorization"] = authHeader 356 | end 357 | 358 | local ok, response_code, response_headers, response_status_line, response_body = PerformRequestHelper(self, url, endpoint.method, headers, arguments, post_body) 359 | 360 | return response_code, response_headers, response_body 361 | end 362 | --]=] 363 | 364 | 365 | --- 366 | -- Exchanges a request token for an Access token (http://tools.ietf.org/html/rfc5849#section-2.3) 367 | -- @param arguments is an optional table with whose keys and values will be encoded as "application/x-www-form-urlencoded" 368 | -- (when doing a POST) or encoded and sent in the query string (when doing a GET). 369 | -- @param headers is an optional table with http headers to be sent in the request 370 | -- @param callback is only required if running under LuaNode. It is a function to be called with a table with the 371 | -- obtained token or [false, http status code, http response headers, http status line and the response body] in case 372 | -- of an error. The callback is mandatory when running under LuaNode. 373 | -- @return nothing if running under LuaNode (the callback will be called instead). Else, a table containing the returned 374 | -- values from the server if succesfull or nil plus the http status code (a number), a table with the response 375 | -- headers, the status line and the response itself 376 | -- 377 | function Client:GetAccessToken(arguments, headers, callback) 378 | 379 | if type(arguments) == "function" then 380 | callback = arguments 381 | arguments, headers = nil, nil 382 | elseif type(headers) == "function" then 383 | callback = headers 384 | headers = nil 385 | end 386 | 387 | if isLuaNode and not callback then 388 | error("GetAccessToken needs a callback") 389 | end 390 | 391 | local args = { 392 | oauth_consumer_key = self.m_consumer_key, 393 | oauth_nonce = generate_nonce(), 394 | oauth_signature_method = self.m_signature_method, 395 | oauth_timestamp = generate_timestamp(), 396 | oauth_version = "1.0", 397 | } 398 | args = merge(args, arguments) 399 | args.oauth_token = (arguments and arguments.oauth_token) or self.m_oauth_token or error("no oauth_token") 400 | args.oauth_verifier = (arguments and arguments.oauth_verifier) or self.m_oauth_verifier -- or error("no oauth_verifier") -- twitter se banca que no venga esto, aunque el RFC dice MUST 401 | 402 | local endpoint = self.m_endpoints.AccessToken 403 | local oauth_token_secret = (arguments and arguments.oauth_token_secret) or self.m_oauth_token_secret or error("no oauth_token_secret") 404 | if arguments then 405 | arguments.oauth_token_secret = nil -- this is never sent 406 | end 407 | args.oauth_token_secret = nil -- this is never sent 408 | 409 | local oauth_signature, post_body, authHeader = self:Sign(endpoint.method, endpoint.url, args, oauth_token_secret) 410 | --print(oauth_signature) 411 | --print(post_body) 412 | --print(authHeader) 413 | 414 | local headers = merge({}, headers) 415 | if self.m_supportsAuthHeader then 416 | headers["Authorization"] = authHeader 417 | end 418 | 419 | if not callback then 420 | 421 | local ok, response_code, response_headers, response_status_line, response_body = PerformRequestHelper(self, endpoint.url, endpoint.method, headers, arguments, post_body) 422 | if not ok or response_code ~= 200 then 423 | -- can't do much, the responses are not standard 424 | return nil, response_code, response_headers, response_status_line, response_body 425 | end 426 | 427 | local values = {} 428 | for key, value in string.gmatch(response_body, "([^&=]+)=([^&=]*)&?" ) do 429 | --print( ("key=%s, value=%s"):format(key, value) ) 430 | values[key] = url_unescape(value) 431 | end 432 | self.m_oauth_token_secret = values.oauth_token_secret 433 | self.m_oauth_token = values.oauth_token 434 | 435 | return values 436 | 437 | else 438 | 439 | local oauth_instance = self 440 | 441 | PerformRequestHelper(self, endpoint.url, endpoint.method, headers, arguments, post_body, 442 | function(err, response_code, response_headers, response_status_line, response_body) 443 | 444 | if err then 445 | callback(err) 446 | return 447 | end 448 | 449 | if response_code ~= 200 then 450 | -- can't do much, the responses are not standard 451 | callback({ status = response_code, headers = response_headers, status_line = response_status_line, body = response_body}) 452 | return 453 | end 454 | local values = {} 455 | for key, value in string.gmatch(response_body, "([^&=]+)=([^&=]*)&?" ) do 456 | --print( ("key=%s, value=%s"):format(key, value) ) 457 | values[key] = url_unescape(value) 458 | end 459 | 460 | oauth_instance.m_oauth_token_secret = values.oauth_token_secret 461 | oauth_instance.m_oauth_token = values.oauth_token 462 | 463 | callback(nil, values) 464 | end) 465 | end 466 | end 467 | 468 | 469 | --- 470 | -- After retrieving an access token, this method is used to issue properly authenticated requests. 471 | -- (see http://tools.ietf.org/html/rfc5849#section-3) 472 | -- @param method is the http method (GET, POST, etc) 473 | -- @param url is the url to request 474 | -- @param arguments is an optional table whose keys and values will be encoded as "application/x-www-form-urlencoded" 475 | -- (when doing a POST) or encoded and sent in the query string (when doing a GET). 476 | -- It can also be a string with the body to be sent in the request (usually a POST). In that case, you need to supply 477 | -- a valid Content-Type header. 478 | -- @param headers is an optional table with http headers to be sent in the request 479 | -- @param callback is only required if running under LuaNode. It is a function to be called with an (optional) error object and the result of the request. 480 | -- @return nothing if running under Luanode (the callback will be called instead). Else, the http status code 481 | -- (a number), a table with the response headers, the status line and the response itself. 482 | -- 483 | function Client:PerformRequest(method, url, arguments, headers, callback) 484 | assert(type(method) == "string", "'method' must be a string") 485 | method = method:upper() 486 | 487 | if type(arguments) == "function" then 488 | callback = arguments 489 | arguments, headers = nil, nil 490 | elseif type(headers) == "function" then 491 | callback = headers 492 | headers = nil 493 | end 494 | 495 | if isLuaNode and not callback then 496 | error("PerformRequest needs a callback") 497 | end 498 | 499 | local headers, arguments, post_body = self:BuildRequest(method, url, arguments, headers) 500 | 501 | if not callback then 502 | local ok, response_code, response_headers, response_status_line, response_body = PerformRequestHelper(self, url, method, headers, arguments, post_body) 503 | return response_code, response_headers, response_status_line, response_body 504 | 505 | else 506 | PerformRequestHelper(self, url, method, headers, arguments, post_body, callback) 507 | end 508 | end 509 | 510 | 511 | --- 512 | -- After retrieving an access token, this method is used to build properly authenticated requests, allowing the user 513 | -- to send them with the method she seems fit. 514 | -- (see http://tools.ietf.org/html/rfc5849#section-3) 515 | -- @param method is the http method (GET, POST, etc) 516 | -- @param url is the url to request 517 | -- @param arguments is an optional table whose keys and values will be encoded as "application/x-www-form-urlencoded" 518 | -- (when doing a POST) or encoded and sent in the query string (when doing a GET). 519 | -- It can also be a string with the body to be sent in the request (usually a POST). In that case, you need to supply 520 | -- a valid Content-Type header. 521 | -- @param headers is an optional table with http headers to be sent in the request 522 | -- @return a table with the headers, a table with the (cleaned up) arguments and the request body. 523 | function Client:BuildRequest(method, url, arguments, headers) 524 | assert(type(method) == "string", "'method' must be a string") 525 | method = method:upper() 526 | 527 | local args = { 528 | oauth_consumer_key = self.m_consumer_key, 529 | oauth_nonce = generate_nonce(), 530 | oauth_signature_method = self.m_signature_method, 531 | oauth_timestamp = generate_timestamp(), 532 | oauth_version = "1.0" 533 | } 534 | local arguments_is_table = (type(arguments) == "table") 535 | if arguments_is_table then 536 | args = merge(args, arguments) 537 | end 538 | args.oauth_token = (arguments_is_table and arguments.oauth_token) or self.m_oauth_token or error("no oauth_token") 539 | local oauth_token_secret = (arguments_is_table and arguments.oauth_token_secret) or self.m_oauth_token_secret or error("no oauth_token_secret") 540 | if arguments_is_table then 541 | arguments.oauth_token_secret = nil -- this is never sent 542 | end 543 | args.oauth_token_secret = nil -- this is never sent 544 | 545 | local oauth_signature, post_body, authHeader = self:Sign(method, url, args, oauth_token_secret) 546 | local headers = merge({}, headers) 547 | if self.m_supportsAuthHeader then 548 | headers["Authorization"] = authHeader 549 | end 550 | 551 | -- Remove oauth_related arguments 552 | if type(arguments) == "table" then 553 | for k,v in pairs(arguments) do 554 | if type(k) == "string" and k:match("^oauth_") then 555 | arguments[k] = nil 556 | end 557 | end 558 | if not next(arguments) then 559 | arguments = nil 560 | end 561 | end 562 | 563 | return headers, arguments, post_body 564 | end 565 | 566 | -- 567 | -- Sets / gets oauth_token 568 | function Client:SetToken(value) 569 | self.m_oauth_token = value 570 | end 571 | function Client:GetToken() 572 | return self.m_oauth_token 573 | end 574 | 575 | -- 576 | -- Sets / gets oauth_token_secret 577 | function Client:SetTokenSecret(value) 578 | self.m_oauth_token_secret = value 579 | end 580 | function Client:GetTokenSecret() 581 | return self.m_oauth_token_secret 582 | end 583 | 584 | -- 585 | -- Sets / gets oauth_verifier 586 | function Client:SetVerifier(value) 587 | self.m_oauth_verifier = value 588 | end 589 | function Client:GetVerifier() 590 | return self.m_oauth_verifier 591 | end 592 | 593 | --- 594 | -- Builds a new OAuth client instance 595 | -- @param consumer_key is the public key 596 | -- @param consumer_secret is the private key 597 | -- @param endpoints is a table containing the URLs where the Service Provider exposes its endpoints 598 | -- each endpoint is either a string (its url, the method is POST by default) or a table, with the url in the array part and the method 599 | -- in the 'method' field. 600 | -- @param params is an optional table with additional parameters: 601 | -- @field SignatureMethod indicates the signature method used by the server (PLAINTEXT, RSA-SHA1, HMAC-SHA1 (default) ) 602 | -- @field UseAuthHeaders indicates if the server supports oauth_xxx parameters to be sent in the 'Authorization' HTTP header (true by default) 603 | -- @return the http status code (a number), a table with the response headers and the response itself 604 | -- 605 | function Client.new(consumer_key, consumer_secret, endpoints, params) 606 | params = params or {} 607 | local newInstance = { 608 | m_consumer_key = consumer_key, 609 | m_consumer_secret = consumer_secret, 610 | m_endpoints = {}, 611 | m_signature_method = params.SignatureMethod or "HMAC-SHA1", 612 | m_supportsAuthHeader = true, 613 | m_oauth_token = params.OAuthToken, 614 | m_oauth_token_secret = params.OAuthTokenSecret, 615 | m_oauth_verifier = params.OAuthVerifier 616 | } 617 | 618 | if type(params.UseAuthHeaders) == "boolean" then 619 | newInstance.m_supportsAuthHeader = params.UseAuthHeaders 620 | end 621 | 622 | for k,v in pairs(endpoints or {}) do 623 | if type(v) == "table" then 624 | newInstance.m_endpoints[k] = { url = v[1], method = string.upper(v.method) } 625 | else 626 | newInstance.m_endpoints[k] = { url = v, method = "POST" } 627 | end 628 | end 629 | 630 | setmetatable(newInstance, Client) 631 | 632 | return newInstance 633 | end 634 | 635 | return Client 636 | --------------------------------------------------------------------------------