Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C++: C++Builder: RESTRequest ja virhe 400 Veikkauksen rajapinnassa

Sivun loppuun

Ylikerroin [25.10.2016 21:28:14]

#

Käytän C++Builder XE6 ja sen REST-komponentteja ohjelmankehitykseen pelatakseni erilaisia Veikkauksen "taitopelejä".

Veikkaus on antanut referenssitoteutuksen (https://github.com/VeikkausOy/sport-games-robot) pythonilla, joka on minulle vieras ohjelmointikieli, joten koitan tehdä ohjelman C++Builderilla.

Veikkauksen pelitilin saldon kyselyn pitäisi onnistua alla olevalla koodilla. Tarvitaan 2 Edit-komponenttia (Username ja password), 1 button ja REST-komponenteista: SimpleAuthenticator, RESTClient, RESTRequest ja RESTResponse.

Loggautuminen onnistuu palvelin palauttaa StatusCode-arvon 200. RESTRequest komponentin arvoja muokataan tämänjälkeen saldokyselyä varten, joka kuitenkin epäonnistuu (virheilmoitus: 'HTTP/1.1 400 Bad Request')

Veikkaukselta kertoivat, että
"Näyttäisi siltä että tuo koodi tekee pyynnön, joka on muotoa /api/v1/players/self/account?= . Eli lopussa on ylimääräinen = merkki", mutta koska he eivät tunne C++Builderia eivät he osanneet sanoa mikä koodissani on väärin.

Osaisiko kukaan kertoa missä vika mahtaa olla alla olevassa koodissa ?
Delphi-osaaja voisi ehkä myös tietää. Kiitos etukäteen.

void __fastcall TForm1::Button1Click(TObject *Sender)
{
	RESTClient1->Authenticator = SimpleAuthenticator1;
	RESTClient1->BaseURL = "https://www.veikkaus.fi";
	RESTClient1->AllowCookies = true;

	TJSONObject *json = new TJSONObject();
	json->AddPair("type", new TJSONString("STANDARD_LOGIN") );
	json->AddPair("login", new TJSONString( Edit1->Text ) );
	json->AddPair("password", new TJSONString( Edit2->Text ) );

	// Login
	RESTRequest1->Params->Clear();
	RESTRequest1->Params->AddItem("X-ESA-API-Key", "ROBOT", pkHTTPHEADER);
	RESTRequest1->Params->AddItem("Accept", "application/json", pkHTTPHEADER);
	RESTRequest1->AddParameter("body", json,  pkREQUESTBODY);
	RESTRequest1->Client = RESTClient1;
	RESTRequest1->Method = rmPOST;
	RESTRequest1->Resource = "api/v1/sessions";
	RESTRequest1->Execute();

	if(  RESTResponse1->StatusCode == 200) // Login onnistui
	{   // Saldokysely
		RESTRequest1->Params->Clear();
		RESTRequest1->Params->AddItem("X-ESA-API-Key", "ROBOT", pkHTTPHEADER);
		RESTRequest1->Params->AddItem("Accept", "application/json", pkHTTPHEADER);
		RESTRequest1->Resource = "api/v1/players/self/account";
		RESTRequest1->Method = rmGET;
		RESTRequest1->Execute();  // virheilmoitus: 'HTTP/1.1 400 Bad Request'.
		RESTResponse1->Content;
	}
	else
	{
		ShowMessage("Kirjautuminen epäonnistui: " + RESTResponse1->ErrorMessage );
	}
}

Mod. lisäsi kooditagit!

Grez [25.10.2016 22:24:31]

#

Voisi kuvitella, että veikkauksen palvelin antaa tuossa kirjautumisen yhteydessä jonkin tunnisteen (eväste, OAuth headeri, tms...), joka pitäisi heittää takaisin palvelimelle seuraavia pyyntöjä tehdessä. Ehkäpä Delphin Restclient ei hoida sitä automaattisesti pyyntöjen välillä.

Pessi [25.10.2016 23:10:04]

#

Veikkauksen APIn mukaan tuo kysely on muotoa GET /api/v1/players/self/account eli tuossa sinun kyselyssä olisi lopussa merkit ?= ylimääräisiä. Jostain tuo koodinpätkä saa päähänsä lisätä nuo vaikka niitä ei tarvita. REST requestissa vaikuttaisi olevan parametrien automaattinen generointi defaulttina päällä, joka voisi nuo merkit lisätä. Kokeiles tuo generointi laittaa pois päältä ja testaa mitä tapahtuu.

Ylikerroin [26.10.2016 23:37:08]

#

Kokeilin seuraavia ehdotettuja vaihtoehtoja alla näkyvässä if-rakenteessa:
a) RESTRequest1->Params->Clear();
b) RESTRequest1->AutoCreateParams = false;
c) RESTRequest1->ResetToDefaults();

if(  RESTResponse1->StatusCode == 200) // Login onnistui
{   // Saldokysely
	//RESTRequest1->Params->Clear();
	//RESTRequest1->AutoCreateParams = false;
	RESTRequest1->ResetToDefaults();
	RESTRequest1->Params->AddItem("X-ESA-API-Key", "ROBOT", pkHTTPHEADER);
	RESTRequest1->Params->AddItem("Accept", "application/json", pkHTTPHEADER);
	RESTRequest1->Resource = "api/players/self/account";
	RESTRequest1->Method = rmGET;

	UnicodeString s = RESTRequest1->GetFullRequestURL();

	RESTRequest1->Execute();  // virheilmoitus: 'HTTP/1.1 400 Bad Request'.
	RESTResponse1->Content;
}

ja debuggerilla katsoin millaisia arvoja funktio antaa muuttujalle s.


1)

RESTRequest1->Params->Clear();
// ...
s == { u"https://www.veikkaus.fi/api/players/self/account?=" }

2)

//RESTRequest1->Params->Clear(); // kommentoitu pois
// ...
s == { u"https://www.veikkaus.fi/api/v1/players/self/account?body=%7B%22type%22%3A%22STANDARD_LOGIN%22%2C%22login%22%3A%22TUNNUS%22%2C%22password%22%3A%22SALASANA%22%7D&=" }

3)

RESTRequest1->Params->Clear();
RESTRequest1->AutoCreateParams = false;
// ...
s == { u"https://www.veikkaus.fi/api/players/self/account?=" }

4)

//RESTRequest1->Params->Clear(); // kommentoitu pois
RESTRequest1->AutoCreateParams = false;
// ...
s == { u"https://www.veikkaus.fi/api/v1/players/self/account?body=%7B%22type%22%3A%22STANDARD_LOGIN%22%2C%22login%22%3A%22TUNNUS%22%2C%22password%22%3A%22SALASANA%22%7D&=" }

5)

//RESTRequest1->Params->Clear(); // kommentoitu pois
//RESTRequest1->AutoCreateParams = false; // kommentoitu pois
RESTRequest1->ResetToDefaults();
{ u"https://www.veikkaus.fi/api/players/self/account" }

Muuttujan arvo 2) ja 4) tapauksessa

s = { u"https://www.veikkaus.fi/api/v1/players/self/account?body=%7B%22type%22%3A%22STANDARD_LOGIN%22%2C%22login%22%3A%22TUNNUS%22%2C%22password%22%3A%22SALASANA%22%7D&=" }

on selkokielisenä

{ u"https://www.veikkaus.fi/api/v1/players/self/account?body={"type":"STANDARD_LOGIN","login":"TUNNUS","password":"SALASANA"}&=" }

Mikään näistä ei kuitenkaan toiminut. Ilmeisesti 3) arvo olisi oikein ilman tuota = merkkiä. Jotenkin nuo login vaiheen evästeet pitäisi olla mukana saldokyselyssä, kuten Grez mainitsi. Hoituuko se automaattisesti kun Reguest-komponentti on kytketty aiemmassa pyynnössä käytettyyn RESTClient:tiin ? Embarcaderon dokumentointi on aika olematonta eikä esimerkkejäkään oikein ele saatavilla.

Mod. huom: käytä kooditageja!

Metabolix [27.10.2016 17:27:01]

#

Jos oikeat osoitteet ja parametrit eivät selviä dokumentaatiosta, kokeile. Pystytä HTTP-välityspalvelin, joka tallentaa kaiken liikenteen ja välittää pyynnöt edelleen Veikkaukselle. Vaihda Veikkauksen mallikoodiin tuon välityspalvelimen osoite (tyyliin localhost:8080) ja katso, millaisia pyyntöjä tulee. Viilaa sitten omaa koodiasi, kunnes saat samanlaiset pyynnöt.

Ja käytä kooditageja foorumilla, jotta viesteistäsi saa jotain selvää.

Silkkikauppias [28.10.2016 23:22:24]

#

Rakentelin taannoin Delphillä Indy-komponentteja käyttäen toimivan ohjelman Veikkauksen pelejä varten. En silloin koodannut saldokyselyä, mutta testasin tänään sen toimivuutta. Onnistuneen kirjautumisen jälkeen saldon saa kysyttyä yksinkertaisesti:

// kirjautuminen

Url := 'https://www.veikkaus.fi/api/v1/sessions';

Teksti := '{"type":"STANDARD_LOGIN","login":"' + EditUsername.Text +
'","password":"' + EditPassword.Text + '"}';

JsonToSend := TStringStream.Create(Utf8Encode(Teksti));
S := IdHTTP1.Post(Url, JsonToSend);
JsonToSend.Free;

// saldon haku

Url := 'https://www.veikkaus.fi/api/v1/players/self/account';
S := IdHTTP1.Get(Url);

C++Builderia en tunne, en liioin REST-komponenttejakaan. Niiden suhteen en osaa auttaa. Tältä pohjalta voisi ajatella, että pyynnössä on jotakin liikaa. Voineeko se, että Veikkaus pyrkii palauttamaan datan pakattuna, vaikuttaa asiaan.

Ylikerroin [31.10.2016 09:57:19]

#

^Kiitos vinkintä, luulen pystyväni muokkaamann antamasi mallin C++Builderille. En ole koskaan käyttänyt Indy-komponentteja aiemmin, tarvitaanko antamasi delphi-koodin testaamiseen muita Indy-komponentteja ja tarvitseeko komponentin oletusarvoja muokata ?

Silkkikauppias [04.11.2016 23:31:51]

#

Testailin näitä juttuja viime viikonloppuna. Minusta ongelmasi kiteytyy pyyntöön "/api/v1/players/self/account?=". Tällä kertaa käytin rest-komponentteja. En pystynyt edes kirjautumaan Veikkauksen järjestelmään. Virheilmoituksena tuli 415 Unsupported media type. Virheen lähdettä en pystynyt eristämään.

Syystä tai toisesta Indy-komponenteilla kirjautuminen ja tilitietojen hakeminen onnnistuu. Ymmärrät varmaankin asian parhaiten koodista. Tarvitaan siis TIdHTTP-komponentti. OpenSSL:n voit tarvittaessa ladata netistä. Voi olla, että uses-listaan joudut käsin lisäämään joitakin uniteja.

var
  SSLIOHandler: TIdSSLIOHandlerSocketOpenSSL;
  Viesti: String;
  JsonT: TJSONObject;
  JsonToSend, Palaute: TStringStream;
  Jatka: Boolean;
begin

  SSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create;
  IdHTTP1.IOHandler := SSLIOHandler;
  IdHTTP1.Compressor := TIdCompressorZLib.Create(IdHTTP1);

  JsonT := TJSONObject.Create;
  JsonT.AddPair('type', 'STANDARD_LOGIN');
  JsonT.AddPair('login', '9999999');
  JsonT.AddPair('password', '9999999');

  Viesti := JsonT.ToString;
  JsonToSend := TStringStream.Create(Viesti, TEncoding.UTF8);
  Palaute := TStringStream.Create('', TEncoding.UTF8);

  with IdHttp1.Request do
  begin
   CustomHeaders.Clear;
   CustomHeaders.AddValue('X-ESA-API-Key', 'ROBOT');
   Accept := 'application/json';
   ContentType := 'application/json';
   Charset := 'UTF-8';
   AcceptCharset := 'UTF-8';
  end;

  try
   IdHTTP1.Post('https://www.veikkaus.fi/api/v1/sessions', JsonToSend,
   Palaute);
   Jatka := True;
   Memo1.Lines.Clear;
   Memo1.Lines.Add(Palaute.DataString);
  except
   on E: Exception do
   begin
    ShowMessage('Lokkautumisvirhe:'#13#10 + e.Message);
    Jatka := False;
   end;
  end;

  JsonToSend.Free;
  Palaute.Free;
  Palaute := TStringStream.Create('', TEncoding.UTF8);

  if Jatka then
  begin

   try
    IdHTTP1.Get('https://www.veikkaus.fi/api/v1/players/self/account',
    Palaute);
   except
    on E: Exception do
    begin
      ShowMessage('Virhe haettaessa saldotietoja:'#13#10 + e.Message);
      Jatka := False;
    end;
   end;

   if Jatka then
   begin
    if Memo1.Lines.Count > 0 then Memo1.Lines.Add('');
    Memo1.Lines.Add(Palaute.DataString);
   end;

  end;

  Palaute.Free;
  ShowMessage('Tehty');

Ylikerroin [06.11.2016 17:20:30]

#

^Kiitoksia vastauksesta, sain saldokyselyn onnistumaan esimerkkisi avulla C++Builderilla :)

Pessi [06.11.2016 20:57:34]

#

Minkäslaisella koodilla lähti toimimaan?

Ylikerroin [06.11.2016 22:52:57]

#

Tuosta näkyvät komponentit ja koodi.

class TForm1 : public TForm
{
__published:	// IDE-managed Components
	TButton *Button1;
	TEdit *Edit1;
	TEdit *Edit2;
	TIdSSLIOHandlerSocketOpenSSL *SSLIOHandler;
	TIdHTTP *IdHTTP1;
	TIdCompressorZLib *IdCompressorZLib1;
	TMemo *Memo1;
	void __fastcall Button1Click(TObject *Sender);
private:	// User declarations
public:		// User declarations
	__fastcall TForm1(TComponent* Owner);
};
void __fastcall TForm1::Button1Click(TObject *Sender)
{
	IdHTTP1->IOHandler = SSLIOHandler;
	IdHTTP1->Compressor = IdCompressorZLib1;

	TJSONObject *JsonT = new TJSONObject();
	JsonT->AddPair("type", new TJSONString("STANDARD_LOGIN") );
	JsonT->AddPair("login", new TJSONString( Edit1->Text ) );
	JsonT->AddPair("password", new TJSONString( Edit2->Text ) );

	UnicodeString Viesti = JsonT->ToString();
	TStringStream* JsonToSend = new TStringStream( Viesti );
	TStringStream* Palaute = new TStringStream();

	IdHTTP1->Request->CustomHeaders->Clear();
	IdHTTP1->Request->CustomHeaders->AddValue("X-ESA-API-Key", "ROBOT");
	IdHTTP1->Request->Accept = "application/json";
	IdHTTP1->Request->ContentType = "application/json";
	IdHTTP1->Request->CharSet = "UTF-8";
	IdHTTP1->Request->AcceptCharSet = "UTF-8";

	bool Jatka = false;
	try
	{
		IdHTTP1->Post("https://www.veikkaus.fi/api/v1/sessions", JsonToSend, Palaute);
		Jatka = true;
		Memo1->Lines->Clear();
		Memo1->Lines->Add( Palaute->DataString );
	}
	catch (...)
	{
		ShowMessage("Lokkautumisvirhe");
		Jatka = false;
	}

	delete JsonToSend;
	delete JsonT;

	if( Jatka )
	{
		try
		{
			Palaute->Clear();
			IdHTTP1->Get("https://www.veikkaus.fi/api/v1/players/self/account",Palaute);
		}
		catch (...)
		{
			ShowMessage("Virhe haettaessa saldotietoja");
			Jatka = false;
		}
	}

	if( Jatka )
	{
		if( Memo1-> Lines->Count > 0 ) Memo1->Lines->Add("");
		Memo1->Lines->Add( Palaute->DataString );
	}
}
//---------------------------------------------------------------------------

Metabolix [06.11.2016 23:23:13]

#

Tuossa näyttää olevan monta new'tä, joilla ei ole vastaavaa delete-riviä. Luulen, että ainakin Palaute pitäisi tuhota. Entä pitäisikö kaikki JSON-oliot tuhota, vai menevätkö ne automaattisesti ulommaisen JsonT:n mukana? Onko mitään syytä ylipäänsä käyttää new'tä, vai voisiko oliot luoda ei-dynaamisesti?

Ylikerroin [07.11.2016 10:12:06]

#

Osa hoituu mielestäni automaattisesti esim. new TJSONString("STANDARD_LOGIN") (TJSONObject hoitaa), Palaute olisi deletetoitava. Muistinhallintaan liittyvät asiat eivät olleet kuitenkaan olennainen asia tässä tapauksessa vaan saada ylipäätään pyynnöt menemään onnistuneesti läpi.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta