/*
* Copyright(c) 1997-2001 Id Software, Inc.
* Copyright(c) 2002 The Quakeforge Project.
* Copyright(c) 2006 Quake2World.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or(at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "client.h"
#define HAVE_CURL
#ifdef __unix__
#include <unistd.h>
#endif
#ifdef HAVE_CURL
#include <curl/curl.h>
CURLM *curlm;
CURL *curl;
#endif
// generic encapsulation for common http response codes
typedef struct response_s {
int code;
char *text;
} response_t;
response_t responses[] = {
{400, "Bad request"}, {401, "Unauthorized"}, {403, "Forbidden"},
{404, "Not found"}, {500, "Internal server error"}, {0, NULL}
};
char curlerr[MAX_STRING_CHARS]; // curl's error buffer
char url[MAX_OSPATH]; // remote url to fetch from
char file[MAX_OSPATH]; // local path to save to
long status, length; // for current transfer
qboolean success;
/*
CL_HttpDownloadRecv
*/
size_t CL_HttpDownloadRecv(void *buffer, size_t size, size_t nmemb, void *p){
return fwrite(buffer, size, nmemb, cls.download);
}
/*
CL_HttpDownload
Queue up an http download. The url is resolved from cls.downloadurl and
the current gamedir. We use cURL's multi interface, even tho we only ever
perform one download at a time, because it is non-blocking.
*/
qboolean CL_HttpDownload(void){
#ifdef HAVE_CURL
char game[64];
if(!curlm)
return false;
if(!curl)
return false;
memset(file, 0, sizeof(file)); // resolve local file name
snprintf(file, sizeof(file) - 1, "%s/%s", FS_Gamedir(), cls.downloadname);
FS_CreatePath(file); // create the directory
if(!(cls.download = fopen(file, "wb"))){
//Com_Warn("Failed to open %s.\n", file);
return false; // and open the file
}
cls.downloadhttp = true;
memset(game, 0, sizeof(game)); // resolve gamedir
strncpy(game, Cvar_VariableString("game"), sizeof(game) - 1);
if(!strlen(game)) // use default if not set
strcpy(game, "default");
memset(url, 0, sizeof(url)); // construct url
snprintf(url, sizeof(url) - 1, "%s/%s/%s", cls.downloadurl, game, cls.downloadname);
// set handle to default state
curl_easy_reset(curl);
// set url from which to retrieve the file
curl_easy_setopt(curl, CURLOPT_URL, url);
// set error buffer so we may print meaningful messages
memset(curlerr, 0, sizeof(curlerr));
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curlerr);
// set the callback for when data is received
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CL_HttpDownloadRecv);
curl_multi_add_handle(curlm, curl);
return true;
#else
return false;
#endif
}
/*
CL_HttpResponseCode
*/
char *CL_HttpResponseCode(long code){
int i = 0;
while(responses[i].code){
if(responses[i].code == code)
return responses[i].text;
i++;
}
return "Unknown";
}
/*
CL_HttpDownloadCleanup
If a download is currently taking place, clean it up. This is called
both to finalize completed downloads as well as abort incomplete ones.
*/
void CL_HttpDownloadCleanup(){
#ifdef HAVE_CURL
char *c;
if(!cls.download || !cls.downloadhttp)
return;
curl_multi_remove_handle(curlm, curl); // cleanup curl
fclose(cls.download); // always close the file
cls.download = NULL;
if(success){
cls.downloadname[0] = 0;
//if(strstr(file, ".pak")) // add new paks to search paths
// FS_AddPakfile(file);
}
else { // retry via legacy udp download
c = strlen(curlerr) ? curlerr : CL_HttpResponseCode(status);
Com_DPrintf("Failed to download %s via HTTP: %s.\n"
"Trying UDP..", cls.downloadname, c);
MSG_WriteByte(&cls.netchan.message, clc_stringcmd);
MSG_WriteString(&cls.netchan.message, va("download %s", cls.downloadname));
unlink(file); // delete partial or empty file
}
cls.downloadpercent = 0;
cls.downloadhttp = false;
status = length = 0;
success = false;
#endif
}
/*
CL_HttpDownloadThink
Process the pending download by giving cURL some time to think.
Poll it for feedback on the transfer to determine what action to take.
If a transfer fails, stuff a stringcmd to download it via UDP. Since
we leave cls.download.tempname in tact during our cleanup, when the
download is parsed back in the client, it will fopen and begin.
*/
void CL_HttpDownloadThink(void){
#ifdef HAVE_CURL
CURLMsg *msg;
int i;
if(!cls.downloadurl[0] || !cls.download)
return; // nothing to do
// process the download as long as data is avaialble
while(curl_multi_perform(curlm, &i) == CURLM_CALL_MULTI_PERFORM){}
// fail fast on any curl error
if(strlen(curlerr)){
CL_HttpDownloadCleanup();
return;
}
// check for http status code
if(!status){
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
if(status > 0 && status != 200){ // 404, 403, etc..
CL_HttpDownloadCleanup();
return;
}
}
// check for completion
while((msg = curl_multi_info_read(curlm, &i))){
if(msg->msg == CURLMSG_DONE){
success = true;
CL_HttpDownloadCleanup();
CL_RequestNextDownload();
return;
}
}
#endif
}
/*
CL_InitHttpDownload
*/
void CL_InitHttpDownload(void){
#ifdef HAVE_CURL
if(!(curlm = curl_multi_init()))
return;
if(!(curl = curl_easy_init()))
return;
#endif
}
/*
CL_ShutdownHttpDownload
*/
void CL_ShutdownHttpDownload(void){
#ifdef HAVE_CURL
CL_HttpDownloadCleanup();
curl_easy_cleanup(curl);
curl = NULL;
curl_multi_cleanup(curlm);
curlm = NULL;
#endif
}