Lua i WykopAPI #2 – logowanie do serwisu

Idziemy dalej. Skrypt, który piszę docelowo ma służyć podstawowej obsłudze mikrobloga z poziomu konsoli. Byłby niczym bez możliwości dodania wpisu. Co jest potrzebne, żeby dodać wpis na mikrobloga? Musimy pierw ładnie poprosić API o wydanie nam klucza użytkownika, mówiąc inaczej – musimy się zalogować poprzez API na swoje konto.

Aby tego dokonać musimy wysłać zapytanie /user/login, w którym jako parametry POST trzeba przekazać login i klucz do konta. Jak pisałem ostatnio – w przypadku podpisywania żądań POST ważna jest kolejność. I tak tutaj podpisem będzie apikeyhttp://a.wykop.pl/user/login/accountkeylogin. Później wystarczy to przekazać przez http.request.
Jednak dla wygody lepiej stworzyć najpierw funkcję do wysyłania danych do serwera wykopu. Aby to zrobić trzeba zebrać szybko kilka podstawowych informacji:

  • co trzeba wysłać?
  • w jaki sposób?
  • jak wygląda podpis?

I tak się okazuje, że będą nam potrzebne trzy funkcje. Jedna do tworzenia sumy kontrolnej zapytania, druga do przygotowania w odpowiedni sposób parametrów POST i trzecia, która to wszystko ładnie zbierze i połączy w odpowiednie zapytanie do serwera. Zacznijmy od najprostszej z nich, czyli funkcji podpisującej.

function sign( url, post )
  if post ~= nil then
    post = post:gsub( "%w+=", "" )
    post = post:gsub( "&", "," )
  else
    post = ""
  end
 
  return md5.sumhexa( secret .. url .. post )
end

Funkcja jest prosta, jedynie mała część „magii” dzieje się na początku, kiedy z zapytania POST trzeba przejść na odpowiednią formę do podpisu. Z formy accountkey=ACCOUNTKEY&login=11mariom przechodzimy do ACCOUNTKEY,11mariom.

Kolejna funkcja jakiej potrzebujemy przygotowuje zapytanie POST. Na co powinniśmy zwrócić uwagę? Kolejność argumentów (w samym POST nie ma znaczenia, jednak ważna jest przy podpisywaniu, a tamta funkcja opiera się na tej) i zakodowanie tekstu do formy URL – inaczej nie wyślemy poprawnie np. wpisu.

function mkpost( post )
  function encode( s )
    s = s:gsub( "\n", "\r\n" )
    s = s:gsub( "([^%w %-%_%.%~])",
          function (c) return string.format ("%%%02X", string.byte(c)) end)
    s = s:gsub( " ", "+" )
 
    return s
  end
 
  local sort = {}
  local req = ""
  local req_sign = ""
 
  for i,v in pairs( post ) do
    table.insert( sort, i )
  end
 
  table.sort( sort )
 
  for i,v in ipairs( sort ) do
    req_sign = string.format( "%s&%s=%s", req_sign, v, post[v] )
    req = string.format( "%s&%s=%s", req, v, encode( post[v] ) )
  end
  req = req:gsub( "^&", "" )
  req_sign = req_sign:gsub( "^&", "" )
 
  return req, req_sign
end

Dzieje się tu dużo. Idąc od góry – najpierw tworzymy lokalną funkcję do zakodowania ciągu znaków do formatu URL – nic szczególnie nowego czy odkrywczego. Niżej tworzymy kilka lokalnych zmiennych – pierwsza z nich sort to tablica, w której przechowamy posortowane klucze (ze względu, że w Lua nie da się narzucić kolejności w tablicy z kluczami innymi niż liczby) i dwie zmienne do przechowania zapytania. Czemu dwie? Ponieważ podczas podpisywania zapytania muszą zostać użyte niezakodowane URL ciągi znaków. Następnie zapełniamy tablicę sort kluczami z tablicy post i ją sortujemy alfabetycznie. Dalej już tworzymy zapytanie przechodząc po wartościach z sort (które są kluczami post). Na sam koniec usuwamy zbędny początkowy **& i zwracamy zmienne req i req_sort.

Ostatnią potrzebną funkcją jest kawałek kodu wysyłający zapytanie do serwera.

function send( type, method, params, post )
  local param = ""
  local meth = nil
  local post_sign = nil
 
  if params ~= nil then
    for k,v in pairs( params ) do
      param = string.format( "%s/%s/%s", param, k, v )
    end
    param = param:gsub( "^/", "" )
  end
 
  if post ~= nil then
    post, post_sign = mkpost( post )
    meth = "POST"
  end
 
  local url = string.format( "http://a.wykop.pl/%s/%s/appkey/%s/%s",
                             type, method, apikey, param )
  local sign = sign( url, post_sign )
 
  local headers = { ["user-agent"] = "mikrocli",
                    ["apisign"] = sign,
                    ["content-length"] = string.len( post or "" ),
                    ["content-type"] = "application/x-www-form-urlencoded" }
 
  local respond = {}
  http.request{ headers = headers,
                url = url,
                method = meth,
                sink = ltn12.sink.table( respond ),
                source = ltn12.source.string( post ) }
 
  return respond[1]
end

Co się tu dzieje? Właściwie niewiele ;) – najpierw sprawdzamy czy są jakieś dodatkowe parametry, jeśli tak – formatujemy je w odpowiedni sposób. Później sprawdzamy czy jest coś do wysłania – jeśli tak to odwołujemy się do funkcji post napisanej wyżej. Dalej tworzymy url, do którego będziemy się zwracać, generujemy podpis zapytania, uzupełniamy nagłówki (co ważne content-length musimy stworzyć sami, bo socket.http tego sam nie robi i content-type, bo bez niego nie zadziała). A na sam koniec łączymy się z serwerem i odbieramy odpowiedź.**

Czas na temat główny dzisiejszego wpisu – logowanie. Jak się zalogować przez API? Jak pisałem wyżej – do user/login trzeba wysłać jako POST login i accountkey. Przy pomocy wyżej przygotowanych funkcji jest to już proste ;)

function getuserkey()
  local respond = send( "user", "login", nil,
                        { ["accountkey"] = ACCOUNTKEY,
                          ["login"] = login } )
  respond = cjson.decode( respond )
 
  return respond["userkey"]
end

Nic skomplikowanego, prawda? Tak wygląda całość:

#!/usr/bin/env lua
local cjson = require "cjson"
local http = require "socket.http"
local md5 = require "md5"
local ltm12 = require "ltn12" 

apikey = KLUCZAPI
secret = SEKRET

function sign( url, post )
  if post ~= nil then
    post = post:gsub( "%w+=", "" )
    post = post:gsub( "&", "," )
  else
    post = ""
  end
 
  return md5.sumhexa( secret .. url .. post )
end

function mkpost( post )
  function encode( s )
    s = s:gsub( "\n", "\r\n" )
    s = s:gsub( "([^%w %-%_%.%~])",
          function (c) return string.format ("%%%02X", string.byte(c)) end)
    s = s:gsub( " ", "+" )
 
    return s
  end
 
  local sort = {}
  local req = ""
  local req_sign = ""
 
  for i,v in pairs( post ) do
    table.insert( sort, i )
  end
 
  table.sort( sort )
 
  for i,v in ipairs( sort ) do
    req_sign = string.format( "%s&%s=%s", req_sign, v, post[v] )
    req = string.format( "%s&%s=%s", req, v, encode( post[v] ) )
  end
  req = req:gsub( "^&", "" )
  req_sign = req_sign:gsub( "^&", "" )
 
  return req, req_sign
end

function send( type, method, params, post )
  local param = ""
  local meth = nil
  local post_sign = nil
 
  if params ~= nil then
    for k,v in pairs( params ) do
      param = string.format( "%s/%s/%s", param, k, v )
    end
    param = param:gsub( "^/", "" )
  end
 
  if post ~= nil then
    post, post_sign = mkpost( post )
    meth = "POST"
  end
 
  local url = string.format( "http://a.wykop.pl/%s/%s/appkey/%s/%s",
                             type, method, apikey, param )
  local sign = sign( url, post_sign )
 
  local headers = { ["user-agent"] = "mikrocli",
                    ["apisign"] = sign,
                    ["content-length"] = string.len( post or "" ),
                    ["content-type"] = "application/x-www-form-urlencoded" }
 
  local respond = {}
  http.request{ headers = headers,
                url = url,
                method = meth,
                sink = ltn12.sink.table( respond ),
                source = ltn12.source.string( post ) }
 
  return respond[1]
end

function getuserkey()
  local respond = send( "user", "login", nil,
                        { ["accountkey"] = ACCOUNTKEY,
                          ["login"] = login } )
  respond = cjson.decode( respond )
 
  return respond["userkey"]
end

To tyle. Następnym razem będzie o wysyłaniu wpisów na mikrobloga. Będzie to już krótszy wpis, bo całą najgorszą robotę mamy za sobą!