diff --git a/http.cpp b/http.cpp index 87f9c4c..429c77f 100644 --- a/http.cpp +++ b/http.cpp @@ -97,12 +97,17 @@ void MoonlightInstance::NvHTTPInit(int32_t callbackId, pp::VarArray args) PostMessage(ret); } -void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, std::string url, bool binaryResponse) +void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, pp::VarArray args) { - char* _url = strdup(url.c_str()); + std::string url = args.Get(0).AsString(); + std::string ppkstr = args.Get(1).AsString(); + bool binaryResponse = args.Get(2).AsBool(); + + PostMessage(pp::Var(url.c_str())); + PHTTP_DATA data = http_create_data(); int err; - + if (data == NULL) { pp::VarDictionary ret; ret.Set("callbackId", pp::Var(callbackId)); @@ -112,7 +117,7 @@ void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, st goto clean_data; } - err = http_request(_url , data); + err = http_request(url.c_str(), ppkstr.c_str(), data); if (err) { pp::VarDictionary ret; ret.Set("callbackId", pp::Var(callbackId)); @@ -148,5 +153,4 @@ void MoonlightInstance::NvHTTPRequest(int32_t /*result*/, int32_t callbackId, st clean_data: http_free_data(data); - free(_url); } diff --git a/libgamestream/errors.h b/libgamestream/errors.h index 837a2b2..4e85d2f 100644 --- a/libgamestream/errors.h +++ b/libgamestream/errors.h @@ -27,3 +27,4 @@ #define GS_IO_ERROR -5 #define GS_NOT_SUPPORTED_4K -6 +#define GS_CERT_MISMATCH -100 diff --git a/libgamestream/http.c b/libgamestream/http.c index 04990da..68b97a1 100644 --- a/libgamestream/http.c +++ b/libgamestream/http.c @@ -59,7 +59,7 @@ static CURLcode sslctx_function(CURL * curl, void * sslctx, void * parm) return CURLE_OK; } -int http_request(char* url, PHTTP_DATA data) { +int http_request(const char* url, const char* ppkstr, PHTTP_DATA data) { int ret; CURL *curl; @@ -67,12 +67,10 @@ int http_request(char* url, PHTTP_DATA data) { if (!curl) return GS_FAILED; - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); curl_easy_setopt(curl, CURLOPT_SSLENGINE_DEFAULT, 1L); curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE,"PEM"); curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM"); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _write_curl); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, *sslctx_function); @@ -86,8 +84,12 @@ int http_request(char* url, PHTTP_DATA data) { curl_easy_setopt(curl, CURLOPT_WRITEDATA, data); curl_easy_setopt(curl, CURLOPT_URL, url); - // HACK: Connecting with TLS v1.2 causes unexpected TLS alerts - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_1); + // Use the pinned certificate for HTTPS + if (ppkstr != NULL) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, ppkstr); + } if (data->size > 0) { free(data->memory); @@ -102,7 +104,9 @@ int http_request(char* url, PHTTP_DATA data) { CURLcode res = curl_easy_perform(curl); - if(res != CURLE_OK) { + if (res == CURLE_SSL_PINNEDPUBKEYNOTMATCH) { + ret = GS_CERT_MISMATCH; + } else if (res != CURLE_OK) { ret = GS_FAILED; } else if (data->memory == NULL) { ret = GS_OUT_OF_MEMORY; diff --git a/libgamestream/http.h b/libgamestream/http.h index 5baa16d..4fde2db 100644 --- a/libgamestream/http.h +++ b/libgamestream/http.h @@ -31,7 +31,7 @@ typedef struct _HTTP_DATA { } HTTP_DATA, *PHTTP_DATA; PHTTP_DATA http_create_data(); -int http_request(char* url, PHTTP_DATA data); +int http_request(const char* url, const char* ppkstr, PHTTP_DATA data); void http_free_data(PHTTP_DATA data); #ifdef __cplusplus diff --git a/libgamestream/pairing.c b/libgamestream/pairing.c index 5dc5742..a680aba 100644 --- a/libgamestream/pairing.c +++ b/libgamestream/pairing.c @@ -54,11 +54,13 @@ static int xml_search(char* data, size_t len, char* node, char** result) { startOffset = strstr(data, startTag); if (startOffset == NULL) { + free(data); return GS_FAILED; } endOffset = strstr(data, endTag); if (endOffset == NULL) { + free(data); return GS_FAILED; } @@ -67,6 +69,7 @@ static int xml_search(char* data, size_t len, char* node, char** result) { *result = malloc(strlen(startOffset + strlen(startTag)) + 1); strcpy(*result, startOffset + strlen(startTag)); + free(data); return GS_OK; } @@ -141,24 +144,73 @@ static bool verifySignature(const unsigned char *data, int dataLength, unsigned } X509* get_cert(PHTTP_DATA data) { - char *result; + char *pemcerthex; - if (xml_search(data->memory, data->size, "plaincert", &result) != GS_OK) + if (xml_search(data->memory, data->size, "plaincert", &pemcerthex) != GS_OK) return NULL; - BIO* bio = BIO_new_mem_buf(result, -1); - free(result); + // Convert cert from hex string to the PEM string and null terminate + int hexstrlen = strlen(pemcerthex); + char *pemcert = malloc(hexstrlen / 2 + 1); + for (int count = 0; count < hexstrlen; count += 2) { + sscanf(&pemcerthex[count], "%2hhx", &pemcert[count / 2]); + } + pemcert[hexstrlen / 2] = 0; + free(pemcerthex); + + // pemcert is referenced, but NOT copied! + BIO* bio = BIO_new_mem_buf(pemcert, -1); if (bio) { X509* cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); BIO_free_all(bio); + free(pemcert); return cert; } else { + free(pemcert); return NULL; } } +static char* x509_to_curl_ppk_string(X509* x509) { + BIO* bio = BIO_new(BIO_s_mem()); + + // Get x509 public key alone in DER format + EVP_PKEY* pubkey = X509_get_pubkey(x509); + i2d_PUBKEY_bio(bio, pubkey); + EVP_PKEY_free(pubkey); + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + + // SHA256 hash the resulting DER string + unsigned char pubkeyhash[32]; + SHA256((unsigned char*)mem->data, mem->length, pubkeyhash); + BIO_free(bio); + + // Base64-encode the resulting SHA256 hash + bio = BIO_new(BIO_s_mem()); + BIO* b64 = BIO_new(BIO_f_base64()); + bio = BIO_push(b64, bio); + BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); + BIO_write(bio, pubkeyhash, sizeof(pubkeyhash)); + BIO_flush(bio); + + BIO_get_mem_ptr(bio, &mem); + + // Assemble the final curl PPK string + const char* prefix = "sha256//"; + char* ret = malloc(strlen(prefix) + mem->length + 1); + memcpy(ret, prefix, strlen(prefix)); + memcpy(&ret[strlen(prefix)], mem->data, mem->length); + ret[strlen(prefix) + mem->length] = 0; + + BIO_free_all(bio); + + return ret; +} + int gs_unpair(const char* address) { int ret = GS_OK; char url[4096]; @@ -167,13 +219,13 @@ int gs_unpair(const char* address) { return GS_OUT_OF_MEMORY; snprintf(url, sizeof(url), "http://%s:47989/unpair?uniqueid=%s", address, g_UniqueId); - ret = http_request(url, data); + ret = http_request(url, NULL, data); http_free_data(data); return ret; } -int gs_pair(int serverMajorVersion, const char* address, const char* pin) { +int gs_pair(int serverMajorVersion, const char* address, const char* pin, char** curl_ppk_string) { int ret = GS_OK; char* result = NULL; X509* server_cert = NULL; @@ -188,7 +240,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) { PHTTP_DATA data = http_create_data(); if (data == NULL) return GS_OUT_OF_MEMORY; - else if ((ret = http_request(url, data)) != GS_OK) + else if ((ret = http_request(url, NULL, data)) != GS_OK) goto cleanup; if ((ret = xml_search(data->memory, data->size, "paired", &result)) != GS_OK) @@ -230,7 +282,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) { bytes_to_hex(challenge_enc, challenge_hex, 16); snprintf(url, sizeof(url), "http://%s:47989/pair?uniqueid=%s&devicename=roth&updateState=1&clientchallenge=%s", address, g_UniqueId, challenge_hex); - if ((ret = http_request(url, data)) != GS_OK) + if ((ret = http_request(url, NULL, data)) != GS_OK) goto cleanup; free(result); @@ -284,7 +336,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) { bytes_to_hex(challenge_response_hash_enc, challenge_response_hex, 32); snprintf(url, sizeof(url), "http://%s:47989/pair?uniqueid=%s&devicename=roth&updateState=1&serverchallengeresp=%s", address, g_UniqueId, challenge_response_hex); - if ((ret = http_request(url, data)) != GS_OK) + if ((ret = http_request(url, NULL, data)) != GS_OK) goto cleanup; free(result); @@ -328,7 +380,7 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) { bytes_to_hex(client_pairing_secret, client_pairing_secret_hex, 16 + 256); snprintf(url, sizeof(url), "http://%s:47989/pair?uniqueid=%s&devicename=roth&updateState=1&clientpairingsecret=%s", address, g_UniqueId, client_pairing_secret_hex); - if ((ret = http_request(url, data)) != GS_OK) + if ((ret = http_request(url, NULL, data)) != GS_OK) goto cleanup; free(result); @@ -341,6 +393,8 @@ int gs_pair(int serverMajorVersion, const char* address, const char* pin) { goto cleanup; } + *curl_ppk_string = x509_to_curl_ppk_string(server_cert); + cleanup: if (ret != GS_OK) gs_unpair(address); diff --git a/libgamestream/pairing.h b/libgamestream/pairing.h index 2619d04..bc80851 100644 --- a/libgamestream/pairing.h +++ b/libgamestream/pairing.h @@ -23,7 +23,7 @@ extern "C" { #endif -int gs_pair(int serverMajorVersion, const char* address, const char* pin); +int gs_pair(int serverMajorVersion, const char* address, const char* pin, char** server_cert_der_string); #ifdef __cplusplus } diff --git a/main.cpp b/main.cpp index 1bd6f5d..b01fb6b 100644 --- a/main.cpp +++ b/main.cpp @@ -257,13 +257,8 @@ void MoonlightInstance::HandleStopStream(int32_t callbackId, pp::VarArray args) } void MoonlightInstance::HandleOpenURL(int32_t callbackId, pp::VarArray args) { - std::string url = args.Get(0).AsString(); - bool binaryResponse = args.Get(1).AsBool(); - m_HttpThreadPool[m_HttpThreadPoolSequence++ % HTTP_HANDLER_THREADS]->message_loop().PostWork( - m_CallbackFactory.NewCallback(&MoonlightInstance::NvHTTPRequest, callbackId, url, binaryResponse)); - - PostMessage(pp::Var (url.c_str())); + m_CallbackFactory.NewCallback(&MoonlightInstance::NvHTTPRequest, callbackId, args)); } void MoonlightInstance::HandlePair(int32_t callbackId, pp::VarArray args) { @@ -272,12 +267,21 @@ void MoonlightInstance::HandlePair(int32_t callbackId, pp::VarArray args) { } void MoonlightInstance::PairCallback(int32_t /*result*/, int32_t callbackId, pp::VarArray args) { - int err = gs_pair(atoi(args.Get(0).AsString().c_str()), args.Get(1).AsString().c_str(), args.Get(2).AsString().c_str()); + char* ppkstr; + int err = gs_pair(atoi(args.Get(0).AsString().c_str()), args.Get(1).AsString().c_str(), args.Get(2).AsString().c_str(), &ppkstr); pp::VarDictionary ret; ret.Set("callbackId", pp::Var(callbackId)); - ret.Set("type", pp::Var("resolve")); - ret.Set("ret", pp::Var(err)); + if (err == 0) { + ret.Set("type", pp::Var("resolve")); + ret.Set("ret", pp::Var(ppkstr)); + free(ppkstr); + } + else { + ret.Set("type", pp::Var("reject")); + ret.Set("ret", pp::Var(err)); + } + PostMessage(ret); } diff --git a/moonlight.hpp b/moonlight.hpp index 7b311bc..e2d1e1a 100644 --- a/moonlight.hpp +++ b/moonlight.hpp @@ -156,7 +156,7 @@ class MoonlightInstance : public pp::Instance, public pp::MouseLock { void LoadCert(const char* certStr, const char* keyStr); void NvHTTPInit(int32_t callbackId, pp::VarArray args); - void NvHTTPRequest(int32_t, int32_t callbackId, std::string url, bool binaryResponse); + void NvHTTPRequest(int32_t, int32_t callbackId, pp::VarArray args); private: static CONNECTION_LISTENER_CALLBACKS s_ClCallbacks; diff --git a/static/js/index.js b/static/js/index.js index 39de85e..113e812 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -237,6 +237,7 @@ function moduleDidLoad() { revivedHost.serverUid = hosts[hostUID].serverUid; revivedHost.externalIP = hosts[hostUID].externalIP; revivedHost.hostname = hosts[hostUID].hostname; + revivedHost.ppkstr = hosts[hostUID].ppkstr; addHostToGrid(revivedHost); } console.log('%c[index.js]', 'color: green;', 'Loaded previously connected hosts'); @@ -277,26 +278,19 @@ function pairTo(nvhttpHost, onSuccess, onFailure) { pairingDialog.close(); }); - console.log('%c[index.js]', 'color: green;', 'Sending pairing request to ' + nvhttpHost.hostname + ' with random number' + randomNumber); - nvhttpHost.pair(randomNumber).then(function(paired) { - if (!paired) { - if (nvhttpHost.currentGame != 0) { - $('#pairingDialogText').html('Error: ' + nvhttpHost.hostname + ' is busy. Stop streaming to pair.'); - } else { - $('#pairingDialogText').html('Error: failed to pair with ' + nvhttpHost.hostname + '.'); - } - console.log('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs - onFailure(); - return; - } - + console.log('%c[index.js]', 'color: green;', 'Sending pairing request to ' + nvhttpHost.hostname + ' with PIN: ' + randomNumber); + nvhttpHost.pair(randomNumber).then(function() { snackbarLog('Pairing successful'); pairingDialog.close(); onSuccess(); }, function(failedPairing) { snackbarLog('Failed pairing to: ' + nvhttpHost.hostname); - console.error('%c[index.js]', 'color: green;', 'Pairing failed, and returned:', failedPairing); - console.error('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs + if (nvhttpHost.currentGame != 0) { + $('#pairingDialogText').html('Error: ' + nvhttpHost.hostname + ' is busy. Stop streaming to pair.'); + } else { + $('#pairingDialogText').html('Error: failed to pair with ' + nvhttpHost.hostname + '.'); + } + console.log('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs onFailure(); }); }); diff --git a/static/js/utils.js b/static/js/utils.js index ee73f51..d3e14f2 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -67,6 +67,7 @@ String.prototype.toHex = function() { function NvHTTP(address, clientUid, userEnteredAddress = '') { console.log('%c[utils.js, NvHTTP Object]', 'color: gray;', this); this.address = address; + this.ppkstr = null; this.paired = false; this.currentGame = 0; this.serverMajorVersion = 0; @@ -111,29 +112,60 @@ function _base64ToArrayBuffer(base64) { NvHTTP.prototype = { refreshServerInfo: function() { + if (this.ppkstr == null) { + return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) { + this._parseServerInfo(retHttp); + }.bind(this)); + } + // try HTTPS first - return sendMessage('openUrl', [this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { + return sendMessage('openUrl', [this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(ret) { if (!this._parseServerInfo(ret)) { // if that fails // try HTTP as a failover. Useful to clients who aren't paired yet - return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) { + return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) { this._parseServerInfo(retHttp); }.bind(this)); } - }.bind(this)); + }.bind(this), + function(error) { + if (error == -100) { // GS_CERT_MISMATCH + // Retry over HTTP + console.warn('%c[utils.js, utils.js, refreshServerInfo]', 'color: gray;', 'Certificate mismatch. Retrying over HTTP', this); + return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) { + this._parseServerInfo(retHttp); + }.bind(this)); + } + }.bind(this)); }, // refreshes the server info using a given address. This is useful for testing whether we can successfully ping a host at a given address refreshServerInfoAtAddress: function(givenAddress) { + if (this.ppkstr == null) { + // Use HTTP if we have no pinned cert + return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) { + return this._parseServerInfo(retHttp); + }.bind(this)); + } + // try HTTPS first - return sendMessage('openUrl', ['https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { + return sendMessage('openUrl', ['https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(ret) { if (!this._parseServerInfo(ret)) { // if that fails - console.log('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Failed to parse serverinfo from HTTPS, falling back to HTTP'); + console.log('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Failed to parse serverinfo from HTTPS, falling back to HTTP'); // try HTTP as a failover. Useful to clients who aren't paired yet - return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) { + return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) { return this._parseServerInfo(retHttp); }.bind(this)); } - }.bind(this)); + }.bind(this), + function(error) { + if (error == -100) { // GS_CERT_MISMATCH + // Retry over HTTP + console.warn('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Certificate mismatch. Retrying over HTTP', this); + return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), this.ppkstr, false]).then(function(retHttp) { + return this._parseServerInfo(retHttp); + }.bind(this)); + } + }.bind(this)); }, // called every few seconds to poll the server for updated info @@ -157,7 +189,7 @@ NvHTTP.prototype = { // Poll for the app list every 10 successful serverinfo polls. // Not including the first one to avoid PCs taking a while to show // as online initially - if (this._pollCount++ % 10 == 1) { + if (this.paired && this._pollCount++ % 10 == 1) { this.getAppListWithCacheFlush(); } @@ -318,7 +350,7 @@ NvHTTP.prototype = { }, getAppListWithCacheFlush: function() { - return sendMessage('openUrl', [this._baseUrlHttps + '/applist?' + this._buildUidStr(), false]).then(function(ret) { + return sendMessage('openUrl', [this._baseUrlHttps + '/applist?' + this._buildUidStr(), this.ppkstr, false]).then(function(ret) { $xml = this._parseXML(ret); $root = $xml.find("root"); @@ -377,6 +409,7 @@ NvHTTP.prototype = { '/appasset?' + this._buildUidStr() + '&appid=' + appId + '&AssetType=2&AssetIdx=0', + this.ppkstr, true ]).then(function(boxArtBuffer) { var reader = new FileReader(); @@ -405,6 +438,7 @@ NvHTTP.prototype = { '/appasset?' + this._buildUidStr() + '&appid=' + appId + '&AssetType=2&AssetIdx=0', + this.ppkstr, true ]); } @@ -423,6 +457,7 @@ NvHTTP.prototype = { '&surroundAudioInfo=' + surroundAudioInfo + '&remoteControllersBitmap=' + gamepadMask + '&gcmap=' + gamepadMask, + this.ppkstr, false ]); }, @@ -434,12 +469,13 @@ NvHTTP.prototype = { '&rikey=' + rikey + '&rikeyid=' + rikeyid + '&surroundAudioInfo=' + surroundAudioInfo, + this.ppkstr, false ]); }, quitApp: function() { - return sendMessage('openUrl', [this._baseUrlHttps + '/cancel?' + this._buildUidStr(), false]) + return sendMessage('openUrl', [this._baseUrlHttps + '/cancel?' + this._buildUidStr(), this.ppkstr, false]) // Refresh server info after quitting because it may silently fail if the // session belongs to a different client. // TODO: We should probably bubble this up to our caller. @@ -460,14 +496,12 @@ NvHTTP.prototype = { pair: function(randomNumber) { return this.refreshServerInfo().then(function() { - if (this.paired) + if (this.paired && this.ppkstr) return true; - if (this.currentGame != 0) - return false; - - return sendMessage('pair', [this.serverMajorVersion.toString(), this.address, randomNumber]).then(function(pairStatus) { - return sendMessage('openUrl', [this._baseUrlHttps + '/pair?uniqueid=' + this.clientUid + '&devicename=roth&updateState=1&phrase=pairchallenge', false]).then(function(ret) { + return sendMessage('pair', [this.serverMajorVersion.toString(), this.address, randomNumber]).then(function(ppkstr) { + this.ppkstr = ppkstr; + return sendMessage('openUrl', [this._baseUrlHttps + '/pair?uniqueid=' + this.clientUid + '&devicename=roth&updateState=1&phrase=pairchallenge', this.ppkstr, false]).then(function(ret) { $xml = this._parseXML(ret); this.paired = $xml.find('paired').html() == "1"; return this.paired;