source: trunk/luci/libs/twitter/luasrc/bbl-twitter.lua @ 1940

Last change on this file since 1940 was 1940, checked in by matthijs, 8 years ago

luci-twitter: Fix Twitter authentication using OAuth.

This commit adds support for authenticating to Twitter using OAuth,
completely replacing the old "Basic" authentication support (which
stopped working some time ago). This makes the luci-twitter app
functional again (though it does require users to re-authenticate
themselves).

Closes: #900

File size: 9.1 KB
Line 
1-- This file was taken from http://github.com/projectgus/bbl-twitter
2-- unmodified (revisioh 2d414750e9209be334930f0556792efa9b1e9ed9)
3
4
5
6-- bbl-twitter "Barebones Lua Twitter"
7--
8-- Copyright (c) 2011 Angus Gratton, released under the MIT License
9-- (see the included file LICENSE.)
10--
11-- See the README.md file for details or visit http://github.com/projectgus/bbl-twitter
12--
13
14local http = require("socket.http")
15
16-- Configuration elements for twitter client
17twitter_config = {
18        openssl = "openssl",
19        -- Resist the temptation of changing this into https since
20        -- socket.http does not support https (and silently ignores whatever
21        -- scheme you put in the url, using HTTP, breaking the signature). At
22        -- least version 2.0.2 does.
23        url = "http://api.twitter.com",
24}
25
26local function join_http_args(args)
27        local first = true
28        local res = ""
29        local ampersand
30
31        for a,v in orderedPairs(args or {}) do
32                if not first then
33                        res = res .. "&"
34                end
35                first = false
36                res = res .. a .. "=" .. url_encode(v)
37        end
38        return res
39end
40
41local function sign_http_args(client, method, url, args)
42        local query = string.format("%s&%s&%s", method, url_encode(url), url_encode(join_http_args(args)))             
43        local cmd = string.format("echo -n \"%s\" | %s sha1 -hmac \"%s&%s\" -binary | %s base64",
44                                                                                 query, twitter_config.openssl,
45                                                                                 client.consumer_secret, client.token_secret or "",
46                                                                                 twitter_config.openssl)
47        local hash = cmd_output(cmd)
48        hash = string.gsub(hash, "\n", "")
49        return join_http_args(args) .. "&oauth_signature=" .. url_encode(hash)
50end
51
52function cmd_output(cmd)
53        local f = assert(io.popen(cmd, 'r'))
54        local res = assert(f:read('*a'))
55        f:close()
56        return res
57end
58
59local function http_get(client, url, args)
60        local argdata = sign_http_args(client, "GET", url, args)
61        if not string.find(url, "?") then
62                url = url .. "?"
63        end
64        local b, c = http.request(url .. argdata)
65        if b and (c ~= 200) then
66                return nil, "Error " .. c .. ": " .. b
67        else
68                return b, c
69        end
70end
71
72local function http_post(client, url, postargs)
73        local b, c = http.request(url, sign_http_args(client, "POST", url, postargs))                                                           
74        if b and (c ~= 200) then
75                return nil, "Error " .. c .. ": " .. b
76        else
77                return b, c
78        end
79end
80
81local function generate_nonce()
82        math.randomseed( os.time() )
83        local src = ""
84        for i=1,32 do
85                src = src .. string.char(string.byte("a")+math.random(0,25))
86        end
87        return src
88end
89
90local function get_base_args(client, args)
91        args = args or {}
92
93        args.oauth_consumer_key=client.consumer_key
94        args.oauth_nonce=generate_nonce()
95        args.oauth_signature_method="HMAC-SHA1"
96        args.oauth_timestamp=os.time()
97        args.oauth_token=client.token_key
98        args.oauth_version="1.0"
99
100        return args
101end
102
103-- Get a request token and secret
104--
105-- callback is the url passed to Twitter where the user is
106-- redirected back to after authorizing. If this is not a webapp and/or you
107-- need to use out-of-band authorization, pass "oob" as the callback
108-- (the user will then be presented a pincode that should be entered
109-- back into the application).
110--
111-- Token is stored in client.req_token and client.req_secret.
112function get_request_token(client, callback)
113        args = get_base_args(client)
114        args.oauth_callback = callback
115        r, e = http_get( client, twitter_config.url .. "/oauth/request_token", args)
116        assert(r, "Could not get OAuth request token: " .. e)
117       
118        client.req_token = string.match(r, "oauth_token=([^&]*)")
119        client.req_secret = string.match(r, "oauth_token_secret=([^&]*)")
120
121        return client
122end
123
124-- Get the url the user should navigate to to authorize the request
125-- token.
126function get_authorize_url(client)
127        assert(client.req_token and client.req_secret, "Cannot authorize request token when there is none")
128        -- The user should visit this url to authorize the token
129        return twitter_config.url .. "/oauth/authorize?" .. join_http_args({oauth_token = client.req_token})
130end
131
132function out_of_band_cli(client)
133        -- Request a token
134        get_request_token(client, 'oob')
135
136        -- Get the url to authorize it
137        local url = get_authorize_url(client)
138
139        print("Open this URL in your browser and enter back the PIN")
140        print(url)
141        io.write("pin >")
142        local req_pin = io.read("*line")
143
144        get_access_token(client, req_pin)
145end
146
147-- Get an access token after obtaining user authorization for a request
148-- token. The verifier is either the "oauth_verifier" parameter passed
149-- to the callback, or the pin entered by the user.
150--
151-- To be able to use this function, you should make sure that both the
152-- client.req_token and client.req_secret are present (and match the
153-- request token the verifier is for).
154--
155-- The obtained access token is stored inside the client, which can be
156-- used to make authenticated request afterwards. To preserve the
157-- authentication for a longer period of time, store the
158-- client.token_key and client.token_secret in persistent storage.
159-- Also, after obtaining an access token, client.user_id and
160-- client.screen_name contain the user_id (numerical) and screen_name
161-- (username) of the authorizing user.
162function get_access_token(client, verifier)
163        assert(client.req_token and client.req_secret, "Can't get access token without request token")
164        -- Sign the access_token request using the request token. Note that
165        -- Twitter does not currently require this, it seems to ignore the
166        -- signature on access_token requests alltogether (which is in
167        -- violation with the OAuth spec and their own documentation). To
168        -- prevent making this code a bad example and problems when Twitter
169        -- ever becomes compliant, we'll do this the proper way.
170        client.token_key = client.req_token
171        client.token_secret = client.req_secret
172
173        args = {
174                oauth_token=client.req_token,
175                oauth_verifier=verifier
176        }
177        s, r = pcall(signed_request, client, "/oauth/access_token", args, "GET")
178        assert(s, "Unable to get access token: " .. r)
179
180        client.token_key = string.match(r, "oauth_token=([^&]*)")
181        client.token_secret = string.match(r, "oauth_token_secret=([^&]*)")
182        client.screen_name = string.match(r, "screen_name=([^&]*)")
183        client.user_id = string.match(r, "userid=([^&]*)")
184        --print("key = " .. client.token_key)
185        --print("secret = " .. client.token_secret)
186        return client
187end
188
189--
190-- Perform a signed (authenticated) request to the twitter API. If the
191-- url starts with /, the Twitter API base url (twitter_config.url) is
192-- automatically prepended.
193--
194-- method can be "GET" or "POST". When no method is specified, a POST
195-- request is made.
196--
197-- Returns the response body when the request was succesful. Raises an
198-- error when the request fails for whatever reason.
199function signed_request(client, url, args, method)
200   assert(client.token_secret, "Cannot perform signed request without token_secret")
201
202        method = method or "POST"
203        args = args or {}
204
205        if (string.sub(url, 1, 1) == "/") then
206                url = twitter_config.url .. url
207        end
208
209        args = get_base_args(client, args)
210        local r, e
211        if (method == "GET") then
212                r, e = http_get(client, url, args)
213        else
214                r, e = http_post(client, url, args)
215        end
216        assert(r, "Unable to perform signed request: " .. e)
217
218        return r
219end
220
221function update_status(client, tweet)
222        signed_request(client, "/1/statuses/update.xml", {status = tweet})
223end
224
225function client(consumer_key, consumer_secret, token_key, token_secret, verifier)
226        local client = {}
227        for j,x in pairs(twitter_config) do client[j] = x end
228        -- args can be set in twitter_config if you want them global
229        client.consumer_key = consumer_key or client.consumer_key
230        client.consumer_secret = consumer_secret or client.consumer_secret
231        client.token_key = token_key or client.token_key
232        client.token_secret = token_secret or client.token_secret
233
234        assert(client.consumer_key and client.consumer_secret, "you need to specify a consumer key and a consumer secret!")
235        return client
236end
237
238
239-------------------
240-- Util functions
241-------------------
242
243-- Taken from http://lua-users.org/wiki/StringRecipes then modified for RFC3986
244function url_encode(str)
245  if (str) then
246          str = string.gsub(str, "([^%w-._~])",
247                                                                function (c) return string.format ("%%%02X", string.byte(c)) end)
248  end
249  return str   
250end
251
252
253--  taken from http://lua-users.org/wiki/SortedIteration
254--[[
255Ordered table iterator, allow to iterate on the natural order of the keys of a
256table.
257
258Example:
259]]
260
261function __genOrderedIndex( t )
262    local orderedIndex = {}
263    for key in pairs(t) do
264        table.insert( orderedIndex, key )
265    end
266    table.sort( orderedIndex )
267    return orderedIndex
268end
269
270function orderedNext(t, state)
271    -- Equivalent of the next function, but returns the keys in the alphabetic
272    -- order. We use a temporary ordered key table that is stored in the
273    -- table being iterated.
274
275    if state == nil then
276        -- the first time, generate the index
277        t.__orderedIndex = __genOrderedIndex( t )
278        key = t.__orderedIndex[1]
279        return key, t[key]
280    end
281    -- fetch the next value
282    key = nil
283    for i = 1,table.getn(t.__orderedIndex) do
284        if t.__orderedIndex[i] == state then
285            key = t.__orderedIndex[i+1]
286        end
287    end
288
289    if key then
290        return key, t[key]
291    end
292
293    -- no more value to return, cleanup
294    t.__orderedIndex = nil
295    return
296end
297
298function orderedPairs(t)
299    -- Equivalent of the pairs() function on tables. Allows to iterate
300    -- in order
301    return orderedNext, t, nil
302end
303
304-- vim: set ts=3 sts=3 sw=3 noexpandtab:
Note: See TracBrowser for help on using the repository browser.