#include #include #include #include <3ds.h> #include #include #include #include "fs.h" #include "error.h" #include "http.h" #include "stringutil.h" #define MAKE_HTTP_USER_AGENT_(major, minor, micro) ("Mozilla/5.0 (Nintendo 3DS; Mobile; rv:10.0) Gecko/20100101 FBI/" #major "." #minor "." #micro) #define MAKE_HTTP_USER_AGENT(major, minor, micro) MAKE_HTTP_USER_AGENT_(major, minor, micro) #define HTTP_USER_AGENT MAKE_HTTP_USER_AGENT(VERSION_MAJOR, VERSION_MINOR, VERSION_MICRO) #define HTTP_TIMEOUT_SEC 15 #define HTTP_TIMEOUT_NS ((u64) HTTP_TIMEOUT_SEC * 1000000000) struct http_context_s { httpcContext httpc; bool compressed; z_stream inflate; u8 buffer[32 * 1024]; u32 bufferSize; }; void http_resolve_redirect(char* oldUrl, const char* redirectTo, size_t size) { if(size > 0) { if(redirectTo[0] == '/') { char* baseEnd = oldUrl; // Find the third slash to find the end of the URL's base; e.g. https://www.example.com/ u32 slashCount = 0; while(*baseEnd != '\0' && (baseEnd = strchr(baseEnd + 1, '/')) != NULL) { slashCount++; if(slashCount == 3) { break; } } // If there are less than 3 slashes, assume the base URL ends at the end of the string; e.g. https://www.example.com if(slashCount != 3) { baseEnd = oldUrl + strlen(oldUrl); } size_t baseLen = baseEnd - oldUrl; if(baseLen < size) { string_copy(baseEnd, redirectTo, size - baseLen); } } else { string_copy(oldUrl, redirectTo, size); } } } Result http_open(http_context* context, const char* url, bool userAgent) { return http_open_ranged(context, url, userAgent, 0, 0); } Result http_open_ranged(http_context* context, const char* url, bool userAgent, u32 rangeStart, u32 rangeEnd) { if(url == NULL) { return R_APP_INVALID_ARGUMENT; } Result res = 0; http_context ctx = (http_context) calloc(1, sizeof(struct http_context_s)); if(ctx != NULL) { char currUrl[1024]; string_copy(currUrl, url, sizeof(currUrl)); char range[64]; if(rangeEnd > rangeStart) { snprintf(range, sizeof(range), "%lu-%lu", rangeStart, rangeEnd); } else { snprintf(range, sizeof(range), "%lu-", rangeStart); } bool resolved = false; u32 redirectCount = 0; while(R_SUCCEEDED(res) && !resolved && redirectCount < 32) { if(R_SUCCEEDED(res = httpcOpenContext(&ctx->httpc, HTTPC_METHOD_GET, currUrl, 1))) { u32 response = 0; if(R_SUCCEEDED(res = httpcSetSSLOpt(&ctx->httpc, SSLCOPT_DisableVerify)) && (!userAgent || R_SUCCEEDED(res = httpcAddRequestHeaderField(&ctx->httpc, "User-Agent", HTTP_USER_AGENT))) && (rangeStart == 0 || R_SUCCEEDED(res = httpcAddRequestHeaderField(&ctx->httpc, "Range", range))) && R_SUCCEEDED(res = httpcAddRequestHeaderField(&ctx->httpc, "Accept-Encoding", "gzip, deflate")) && R_SUCCEEDED(res = httpcSetKeepAlive(&ctx->httpc, HTTPC_KEEPALIVE_ENABLED)) && R_SUCCEEDED(res = httpcBeginRequest(&ctx->httpc)) && R_SUCCEEDED(res = httpcGetResponseStatusCodeTimeout(&ctx->httpc, &response, HTTP_TIMEOUT_NS))) { if(response == 301 || response == 302 || response == 303) { redirectCount++; char redirectTo[1024]; memset(redirectTo, '\0', sizeof(redirectTo)); if(R_SUCCEEDED(res = httpcGetResponseHeader(&ctx->httpc, "Location", redirectTo, sizeof(redirectTo)))) { httpcCloseContext(&ctx->httpc); http_resolve_redirect(currUrl, redirectTo, sizeof(currUrl)); } } else { resolved = true; if(response == 200) { char encoding[32]; if(R_SUCCEEDED(httpcGetResponseHeader(&ctx->httpc, "Content-Encoding", encoding, sizeof(encoding)))) { bool gzip = strncmp(encoding, "gzip", sizeof(encoding)) == 0; bool deflate = strncmp(encoding, "deflate", sizeof(encoding)) == 0; ctx->compressed = gzip || deflate; if(ctx->compressed) { memset(&ctx->inflate, 0, sizeof(ctx->inflate)); if(deflate) { inflateInit(&ctx->inflate); } else if(gzip) { inflateInit2(&ctx->inflate, MAX_WBITS | 16); } } } } else { res = R_APP_HTTP_ERROR_BASE + response; } } } if(R_FAILED(res)) { httpcCloseContext(&ctx->httpc); } } } if(R_SUCCEEDED(res) && redirectCount >= 32) { res = R_APP_HTTP_TOO_MANY_REDIRECTS; } if(R_FAILED(res)) { free(ctx); } } else { res = R_APP_OUT_OF_MEMORY; } if(R_SUCCEEDED(res)) { *context = ctx; } return res; } Result http_close(http_context context) { if(context == NULL) { return R_APP_INVALID_ARGUMENT; } if(context->compressed) { inflateEnd(&context->inflate); } Result res = httpcCloseContext(&context->httpc); free(context); return res; } Result http_get_size(http_context context, u32* size) { if(context == NULL || size == NULL) { return R_APP_INVALID_ARGUMENT; } return httpcGetDownloadSizeState(&context->httpc, NULL, size); } Result http_get_file_name(http_context context, char* out, u32 size) { if(context == NULL || out == NULL) { return R_APP_INVALID_ARGUMENT; } Result res = 0; char* header = (char*) calloc(1, size + 64); if(header != NULL) { if(R_SUCCEEDED(res = httpcGetResponseHeader(&context->httpc, "Content-Disposition", header, size + 64))) { char* start = strstr(header, "filename="); if(start != NULL) { char format[32]; snprintf(format, sizeof(format), "filename=\"%%%lu[^\"]\"", size); if(sscanf(start, format, out) != 1) { res = R_APP_BAD_DATA; } } else { res = R_APP_BAD_DATA; } } free(header); } else { res = R_APP_OUT_OF_MEMORY; } return res; } Result http_read(http_context context, u32* bytesRead, void* buffer, u32 size) { if(context == NULL || buffer == NULL) { return R_APP_INVALID_ARGUMENT; } Result res = 0; u32 startPos = 0; if(R_SUCCEEDED(res = httpcGetDownloadSizeState(&context->httpc, &startPos, NULL))) { res = HTTPC_RESULTCODE_DOWNLOADPENDING; u32 outPos = 0; if(context->compressed) { u32 lastPos = context->bufferSize; while(res == HTTPC_RESULTCODE_DOWNLOADPENDING && outPos < size) { if((context->bufferSize > 0 || R_SUCCEEDED(res = httpcReceiveDataTimeout(&context->httpc, &context->buffer[context->bufferSize], sizeof(context->buffer) - context->bufferSize, HTTP_TIMEOUT_NS)) || res == HTTPC_RESULTCODE_DOWNLOADPENDING)) { Result posRes = 0; u32 currPos = 0; if(R_SUCCEEDED(posRes = httpcGetDownloadSizeState(&context->httpc, &currPos, NULL))) { context->bufferSize += currPos - lastPos; context->inflate.next_in = context->buffer; context->inflate.next_out = buffer + outPos; context->inflate.avail_in = context->bufferSize; context->inflate.avail_out = size - outPos; inflate(&context->inflate, Z_SYNC_FLUSH); memcpy(context->buffer, context->buffer + (context->bufferSize - context->inflate.avail_in), context->inflate.avail_in); context->bufferSize = context->inflate.avail_in; lastPos = currPos; outPos = size - context->inflate.avail_out; } else { res = posRes; } } } } else { while(res == HTTPC_RESULTCODE_DOWNLOADPENDING && outPos < size) { if(R_SUCCEEDED(res = httpcReceiveDataTimeout(&context->httpc, &((u8*) buffer)[outPos], size - outPos, HTTP_TIMEOUT_NS)) || res == HTTPC_RESULTCODE_DOWNLOADPENDING) { Result posRes = 0; u32 currPos = 0; if(R_SUCCEEDED(posRes = httpcGetDownloadSizeState(&context->httpc, &currPos, NULL))) { outPos = currPos - startPos; } else { res = posRes; } } } } if(res == HTTPC_RESULTCODE_DOWNLOADPENDING) { res = 0; } if(R_SUCCEEDED(res) && bytesRead != NULL) { *bytesRead = outPos; } } return res; } #define R_HTTP_TLS_VERIFY_FAILED 0xD8A0A03C typedef struct { u32 bufferSize; void* userData; Result (*callback)(void* userData, void* buffer, size_t size); Result (*checkRunning)(void* userData); Result (*progress)(void* userData, u64 total, u64 curr); void* buf; u32 pos; Result res; } http_curl_data; static size_t http_curl_write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { http_curl_data* curlData = (http_curl_data*) userdata; if(curlData->checkRunning != NULL && R_FAILED(curlData->res = curlData->checkRunning(curlData->userData))) { return 0; } size_t srcPos = 0; size_t available = size * nmemb; while(available > 0) { size_t remaining = curlData->bufferSize - curlData->pos; size_t copySize = available < remaining ? available : remaining; memcpy((u8*) curlData->buf + curlData->pos, ptr + srcPos, copySize); curlData->pos += copySize; srcPos += copySize; available -= copySize; if(curlData->pos == curlData->bufferSize) { curlData->res = curlData->callback(curlData->userData, curlData->buf, curlData->bufferSize); curlData->pos = 0; } } return R_SUCCEEDED(curlData->res) ? size * nmemb : 0; } int http_curl_xfer_info_callback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { http_curl_data* curlData = (http_curl_data*) clientp; if(curlData->progress != NULL) { curlData->progress(curlData->userData, dltotal, dlnow); } return 0; } Result http_download_callback(const char* url, u32 bufferSize, void* userData, Result (*callback)(void* userData, void* buffer, size_t size), Result (*checkRunning)(void* userData), Result (*progress)(void* userData, u64 total, u64 curr)) { Result res = 0; void* buf = malloc(bufferSize); if(buf != NULL) { http_context context = NULL; if(R_SUCCEEDED(res = http_open(&context, url, true))) { u32 dlSize = 0; if(R_SUCCEEDED(res = http_get_size(context, &dlSize))) { if(progress != NULL) { progress(userData, dlSize, 0); } u32 total = 0; u32 currSize = 0; while(total < dlSize && (checkRunning == NULL || R_SUCCEEDED(res = checkRunning(userData))) && R_SUCCEEDED(res = http_read(context, &currSize, buf, bufferSize)) && R_SUCCEEDED(res = callback(userData, buf, currSize))) { if(progress != NULL) { progress(userData, dlSize, total); } total += currSize; } Result closeRes = http_close(context); if(R_SUCCEEDED(res)) { res = closeRes; } } } else if(res == R_HTTP_TLS_VERIFY_FAILED) { res = 0; CURL* curl = curl_easy_init(); if(curl != NULL) { http_curl_data curlData = {bufferSize, userData, callback, checkRunning, progress, buf, 0, 0}; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_USERAGENT, HTTP_USER_AGENT); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, bufferSize); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, HTTP_TIMEOUT_SEC); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_curl_write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*) &curlData); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, http_curl_xfer_info_callback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, (void*) &curlData); CURLcode ret = curl_easy_perform(curl); if(ret == CURLE_OK && curlData.pos != 0) { curlData.res = curlData.callback(curlData.userData, curlData.buf, curlData.pos); curlData.pos = 0; if(R_FAILED(curlData.res)) { ret = CURLE_WRITE_ERROR; } } if(ret != CURLE_OK) { if(ret == CURLE_WRITE_ERROR) { res = curlData.res; } else if(ret == CURLE_HTTP_RETURNED_ERROR) { long responseCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); res = R_APP_HTTP_ERROR_BASE + responseCode; } else { res = R_APP_CURL_ERROR_BASE + ret; } } curl_easy_cleanup(curl); } else { res = R_APP_CURL_INIT_FAILED; } } free(buf); } else { res = R_APP_OUT_OF_MEMORY; } return res; } typedef struct { void* buf; size_t size; size_t pos; } http_buffer_data; static Result http_download_buffer_callback(void* userData, void* buffer, size_t size) { http_buffer_data* data = (http_buffer_data*) userData; size_t remaining = data->size - data->pos; size_t copySize = size; if(copySize > remaining) { copySize = remaining; } if(copySize > 0) { memcpy((u8*) data->buf + data->pos, buffer, copySize); data->pos += copySize; } return 0; } Result http_download_buffer(const char* url, u32* downloadedSize, void* buf, size_t size) { http_buffer_data data = {buf, size, 0}; Result res = http_download_callback(url, size, &data, http_download_buffer_callback, NULL, NULL); if(R_SUCCEEDED(res)) { *downloadedSize = data.pos; } return res; } Result http_download_json(const char* url, json_t** json, size_t maxSize) { if(url == NULL || json == NULL) { return R_APP_INVALID_ARGUMENT; } Result res = 0; char* text = (char*) calloc(sizeof(char), maxSize); if(text != NULL) { u32 textSize = 0; if(R_SUCCEEDED(res = http_download_buffer(url, &textSize, text, maxSize))) { json_error_t error; json_t* parsed = json_loads(text, 0, &error); if(parsed != NULL) { *json = parsed; } else { res = R_APP_PARSE_FAILED; } } free(text); } else { res = R_APP_OUT_OF_MEMORY; } return res; } static Result FSUSER_AddSeed(u64 titleId, const void* seed) { u32 *cmdbuf = getThreadCommandBuffer(); cmdbuf[0] = 0x087A0180; cmdbuf[1] = (u32) (titleId & 0xFFFFFFFF); cmdbuf[2] = (u32) (titleId >> 32); memcpy(&cmdbuf[3], seed, 16); Result ret = 0; if(R_FAILED(ret = svcSendSyncRequest(*fsGetSessionHandle()))) return ret; ret = cmdbuf[1]; return ret; } Result http_download_seed(u64 titleId) { char pathBuf[64]; snprintf(pathBuf, 64, "/fbi/seed/%016llX.dat", titleId); Result res = 0; FS_Path* fsPath = fs_make_path_utf8(pathBuf); if(fsPath != NULL) { u8 seed[16]; Handle fileHandle = 0; if(R_SUCCEEDED(res = FSUSER_OpenFileDirectly(&fileHandle, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, ""), *fsPath, FS_OPEN_READ, 0))) { u32 bytesRead = 0; res = FSFILE_Read(fileHandle, &bytesRead, 0, seed, sizeof(seed)); FSFILE_Close(fileHandle); } fs_free_path_utf8(fsPath); if(R_FAILED(res)) { u8 region = CFG_REGION_USA; CFGU_SecureInfoGetRegion(®ion); if(region <= CFG_REGION_TWN) { static const char* regionStrings[] = {"JP", "US", "GB", "GB", "HK", "KR", "TW"}; char url[128]; snprintf(url, 128, "https://kagiya-ctr.cdn.nintendo.net/title/0x%016llX/ext_key?country=%s", titleId, regionStrings[region]); u32 downloadedSize = 0; if(R_SUCCEEDED(res = http_download_buffer(url, &downloadedSize, seed, sizeof(seed))) && downloadedSize != sizeof(seed)) { res = R_APP_BAD_DATA; } } else { res = R_APP_OUT_OF_RANGE; } } if(R_SUCCEEDED(res)) { res = FSUSER_AddSeed(titleId, seed); } } else { res = R_APP_OUT_OF_MEMORY; } return res; }