Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Last active July 6, 2023 14:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ariankordi/b16741080b99af032d4ebb68d61ce53c to your computer and use it in GitHub Desktop.
Save ariankordi/b16741080b99af032d4ebb68d61ce53c to your computer and use it in GitHub Desktop.
cronchyroll reverse proxy to 1. fool the crunchyroll apple tv app, 2. use someone else's premium account to get anime but NOT for anything else (watch history, queue, etc). use at your own risk lol
server.modules = (
"mod_proxy",
"mod_cgi",
"mod_rewrite",
#"mod_setenv",
#"mod_openssl"
)
#ssl.engine = "enable"
#ssl.pemfile = "cronch.pem"
server.errorlog = "/dev/stderr"
server.document-root = "/dev/shm"
#server.port = 443
server.port = 8002
#proxy_host = "api.crunchyroll.com"
#proxy_host = "104.18.23.9"
#proxy_host = "127.0.0.1"
#proxy_port = "8001"
#proxy_port = "80"
#endpoints_requiring_cgi = "^/(authenticate|login|logout)\.0\.json"
#setenv.add-environment = ("PROXY_HOST" => proxy_host, "PROXY_PORT" => proxy_port)
url.rewrite-once = (
"^" => "/cgi-bin/info-alt.cgi",
)
$HTTP["url"] == "/cgi-bin/info-alt.cgi" {
cgi.assign = ("" => "")
}
# else {
# proxy.forwarded = ( "for" => 0,
# "proto" => 0,
# "host" => 0
# )
# proxy.server = ("" => (("host" => proxy_host, "port" => proxy_port)))
#}
# notice for this file: replace all fields for certificate, resolver, fastcgi port, php filename, and possibly more
worker_processes auto;
events {}
http {
access_log off;
#error_log /tmp/aror.log;
proxy_ssl_server_name on;
resolver 1.1.1.1;
proxy_buffering off;
server {
listen 443 ssl http2;
ssl_certificate /jffs/cronch/cronch.pem;
ssl_certificate_key /jffs/cronch/cronch.pem;
set $api api.crunchyroll.com;
location / {
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://$api;
location ~ ^/(authenticate|login|logout)\.0\.json {
proxy_set_header Accept-Encoding "";
proxy_pass https://$api;
sub_filter_once on;
sub_filter_types application/json;
sub_filter 'um":""' 'um":"anime"';
}
}
location = /info.0.json {
fastcgi_pass unix:/tmp/cum.sock;
fastcgi_param SCRIPT_FILENAME /tmp/info-alt.php;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
}
}
}
so sooooomeone likes reverse proxies :)
as the description suggests, this is built for the crunchyroll apple tv app which uses the older api.crunchyroll.com API that they are trying to phase out right now
it uses nginx to do most of the reverse proxying with a little help from two php scripts: one does the main thing, and the other is a reverse proxy literally in php meant as a fallback (surprisingly, no one has tried to do this before, LOL.)
what it does is two things at once: allows you to log into the apple tv app without premium (this is what the sub_filter in nginx is for), and proxies your media through another person's premium account, which is what info-alt does (proxy.php is only invoked by info-alt.php.) the info-alt name is because it proxies ONLY an endpoint called info.0.json
so using this requires installing a certificate on your apple tv for the mitm to even work, and then to redirect the dns resolution of api.crunchyroll.com to the reverse proxy that runs these scripts, and that's the setup that i have at this time
these scripts would probably have to be heavily modified and manhandled for another person to be able to use it but either way i hope it might be helpful to someone, i've been using it with minimal issues so far, so enjoy
<?php
//define('API_BASE', 'https://api.crunchyroll.com');
// api.crunchyroll.com resolution, 'raw ip' cloudflare server to pass to
// this is used in my case cause my target device doesn't resolve this correctly
define('API_BASE', 'https://api.crunchyroll.com');
//define('API_RESLOVED', 'api.crunchyroll.com:443:104.18.40.133');
// mitmproxy or busybox instance for testing
//define('API_BASE', 'http://192.168.2.20:8080');
define('DEVICE_ID', '82a109d3-2b05-4a65-a470-708d39dd1dd8');
// information about the device included in auth requests
define('AUTH_POST_DATA',
// apple tvos cause that's what i'm accessing on anyway
'device_type=com.crunchyroll.tvos&access_token=b78a05ad1fd6717'
. '&device_id=' . DEVICE_ID
);
define('DEFAULT_LOCALE', 'enUS');
// crunchyroll credentials in question to pass through
define('API_CREDENTIALS',
// premium account ideally
'&account=username'
. '&password=password'
);
define('CURL_DEFAULT_OPTIONS', array(
CURLOPT_RETURNTRANSFER => true,
//CURLOPT_SSL_VERIFYPEER => false,
//CURLOPT_RESOLVE => array(API_RESLOVED),
CURLOPT_POST => true
//CURLOPT_USERAGENT => 'User-Agent: Crunchyroll/2.7.0 (com.crunchyroll.iphone; build:7; tvOS 14.7.0) Alamofire/5.4.4'
));
// hide errors, particularly about file_get_contents not working
// so that the output is still valid json in normal circumstances
//ini_set('display_errors', 'Off');
// called when giving up the request via the alt and trying to instead pass it unmodified
function giveUp() {
$backtrace = debug_backtrace()[0];
// "giveUp at line 63" etc.
trigger_error($backtrace['function'] . ' at line ' . $backtrace['line']);
require_once 'proxy-curl.php';
//die('give up');
exit();
}
// determine if to give up; if request uri isn't info
if(substr($_SERVER['REQUEST_URI'], 0, 12) !== '/info.0.json' ||
// or if fields is not in either post or get
!array_key_exists('fields', $_REQUEST) ||
// and finally, if fields is not media.stream_data
$_REQUEST['fields'] !== 'media.stream_data') {
// give up now
giveUp();
}
//echo 'satisfactory';
// array where saved auth is stored just like in the file
$saved = array(
'session_id' => null,
'auth' => null,
'auth_expiry' => null
);
// will make a file name like /tmp/cronch-(device id)
$savedFileName = sys_get_temp_dir() . '/cronch-' . DEVICE_ID;
if(file_exists($savedFileName)) {
$savedFileContents = file_get_contents($savedFileName);
if($savedFileContents) {
// unserialize and load the file when it did load correctly
$saved = unserialize($savedFileContents);
}
}
// this is to be set true when the array is modified
// and this is read at the end and the file is saved according to this
$savedPendingSave = false;
//print_r($saved);
// counter for attempts to do target request so it doesn't do too many
//$targetAttemptsCounter = 0;
// total goto limit counter that must not exceed 3 so that no infinite loop happens ideally
$startSessionAttemptsCounter = 0;
function startSessionFlow() {
global $startSessionAttemptsCounter, $saved, $savedPendingSave;
//echo 'it begins' . PHP_EOL;
$startSessionAttemptsCounter++;
if($startSessionAttemptsCounter > 3) {
trigger_error('start session attempts exceeded');
giveUp();
}
// this is a counter to ensure this does not run more than three times and go in an infinite loop and Die
/*if($targetAttemptsCounter > 3) {
//echo 'attempts exceeded!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' . PHP_EOL;
giveUp();
}*/
// if no auth or if current time is more than auth expiry minus 24 hours
if(!$saved['auth'] || ($saved['auth_expiry'] - 86400) < time()) {
$curl = curl_init(API_BASE . '/login.0.json');
curl_setopt_array($curl, CURL_DEFAULT_OPTIONS);
curl_setopt($curl, CURLOPT_POSTFIELDS, AUTH_POST_DATA . API_CREDENTIALS);
$result = curl_exec($curl);
if(!$result) {
trigger_error('login response is falsey (curl error: ' . curl_error($curl) . ')');
curl_close($curl);
giveUp();
}
curl_close($curl);
// json throw on error is not compatible with php 7.2
$resultJSON = json_decode($result/*, null, 4, JSON_THROW_ON_ERROR*/);
if(!$resultJSON || !property_exists($resultJSON, 'data') || !property_exists($resultJSON->data, 'expires')) {
// api exception here means that the credentials may have been incorrect
// and so in this case this will just not work
trigger_error('login response is not json or doesn\'t have required data. here is what we got:' . $result);
giveUp();
}
$saved['auth'] = $resultJSON->data->auth;
// convert expiry from a format like this: 2022-05-06T18:30:28-07:00
$saved['auth_expiry'] = strtotime($resultJSON->data->expires);
$savedPendingSave = true;
}
//echo 'no saved session id, apiStartSession(), before:' . $saved['session_id'] . PHP_EOL;
$curl = curl_init(API_BASE . '/start_session.0.json');
curl_setopt_array($curl, CURL_DEFAULT_OPTIONS);
curl_setopt($curl, CURLOPT_POSTFIELDS, AUTH_POST_DATA
. '&auth=' . urlencode($saved['auth']));
$result = curl_exec($curl);
if(!$result) {
trigger_error('login response is falsey (curl error: ' . curl_error($curl) . ')');
curl_close($curl);
giveUp();
}
curl_close($curl);
$resultJSON = json_decode($result);
if(!$resultJSON || !property_exists($resultJSON, 'data') || !property_exists($resultJSON->data, 'session_id')) {
trigger_error('start session response is not json or doesn\'t have required data. here is what we got:' . $result);
giveUp();
}
// if user is null then the session begin did not work
if(!$resultJSON->data->user) {
//throw new APIException('user was null in session response, auth is incorrect');
//exit('dying after api exception session');
// start session failed = log in again
$saved['auth'] = null;
startSessionFlow();
return;
}
// make sure to save the auth returned by start_session if it was present
if($resultJSON->data->auth) {
$saved['auth'] = $resultJSON->data->auth;
// inferring that expires is there too
$saved['auth_expiry'] = strtotime($resultJSON->data->expires);
}
$saved['session_id'] = $resultJSON->data->session_id;
$savedPendingSave = true;
//echo 'session id after: ' . $saved['session_id'] . PHP_EOL;
}
// no session id = restart auth process
if(!$saved['session_id']) {
startSessionFlow();
}
$finalResult = '';
function performTargetRequest() {
global $targetAttemptsCounter, $saved, $savedPendingSave, $finalResult;
$targetAttemptsCounter++;
//echo 'do target request w session ' . $saved['session_id'] . PHP_EOL;
$locale = DEFAULT_LOCALE;
// locale has to be correct for different dubs to work apparently?
// i don't think this works actually lol but i'm including anyway
if(array_key_exists('locale', $_REQUEST)) {
$locale = $_REQUEST['locale'];
}
$postData =
// without locale it occasionally gives an error in german
'locale=' . $locale
. '&session_id=' . $saved['session_id']
. '&fields=' . $_REQUEST['fields']
. '&media_id=' . $_REQUEST['media_id']
;
$curl = curl_init(API_BASE . '/info.0.json');
curl_setopt_array($curl, CURL_DEFAULT_OPTIONS);
curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
$finalResult = curl_exec($curl);
if(!$finalResult) {
trigger_error('info response is falsey (curl error: ' . curl_error($curl) . ')');
curl_close($curl);
giveUp();
}
curl_close($curl);
$haystack = substr($finalResult, 0, 64);
// just check that there's no occurrence of `error":t` in first 64 characters
// if it DOES occur,
if(strpos($haystack, 'error":t') !== false) {
// reset session_id and go back in case of either forbidden or bad session
// actually i don't know if forbidden actually means you're not authenticates
/*if(strpos($haystack, 'code":"forbidden') !== false || */
if(strpos($haystack, 'code":"bad_session') !== false) {
trigger_error('error code bad_session in response: ' . $finalResult);
// continuing if error code is forbidden
// session was probably incorrect
//echo 'resetting session_id, goto startSession' . PHP_EOL;
$saved['session_id'] = null;
//goto startSession;
startSessionFlow();
performTargetRequest();
return;
} else {
// give up if error code is not forbidden
trigger_error('error found in first 64 chars of json response: ' . $finalResult);
giveUp();
}
}
}
header('Content-Type: application/json');
performTargetRequest();
// save at the very end of this cycle
if($savedPendingSave) {
file_put_contents($savedFileName, serialize($saved));
//echo 'saved'.PHP_EOL;
}
echo $finalResult;
//print_r($http_response_header_global);
// go through http headers in the global scope that were defined by apiRequest
/*foreach($http_response_header_global as $header) {
//echo $header . PHP_EOL;
if(strtolower(substr($header, 0, 14)) === 'content-type: ') {
//echo 'this is it: ' . $header . PHP_EOL;
// set the content type header that was returned
header($header);
}
}*/
/*if($includeHeaders) {
curl_setopt($curl, CURLOPT_HEADERFUNCTION,
function($curl, $header) use (&$headers) {
global $http_response_header_global;
array_push($http_response_header_global, $header);
return strlen($header);
}
);
}*/
// wrapper function that makes a request to the api handling potential errors
/*function apiRequest($endpoint, $postData) {
//$response = file_get_contents(API_BASE . $endpoint, false, $context);
$curl = curl_init(API_BASE . $endpoint);
// set the curl options
curl_setopt_array($curl, CURL_DEFAULT_OPTIONS);
curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
// execute the request
$response = curl_exec($curl);
// if the socket cannot connect then it won't throw an exception apparently but it will be false so i am throwing an exception and returning false so code won't continue
if(!$response) {
trigger_error('response is falsey (curl error: ' . curl_error($curl) . ')');
curl_close($curl);
giveUp();
}
curl_close($curl);
//echo 'SHOULD NOT RUN WHEN THERE IS AN AROR!!!!!!!!!!!! ' . PHP_EOL;
return $response;
}*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//#include <assert.h>
// strptime wasn't working without this
#define _XOPEN_SOURCE
#define __USE_XOPEN
#include <time.h>
#include <json-c/json.h>
#include <curl/curl.h>
#include <pthread.h>
// information about the device included in auth requests
//#define API_BASE "https://api.crunchyroll.com"
// plain api endpoint used for most reverse proxy requests
#define API_BASE "http://api.crunchyroll.com"
#define API_BASE_HTTPS "https://api.crunchyroll.com"
//#define DEVICE_ID "just make a uuid and put it here"
#define DEVICE_ID "82a109d3-2b05-4a65-a470-708d39dd1dd8"
#define AUTH_POST_DATA "device_type=com.crunchyroll.tvos" \
"&access_token=b78a05ad1fd6717" \
"&device_id=" DEVICE_ID
#define DEFAULT_LOCALE "enUS"
// crunchyroll credentials in question to pass through
#define API_CREDENTIALS \
"&account=username" \
"&password=password"
// derived from device id
#define SAVED_FILE_NAME "/tmp/cronch-" DEVICE_ID
#define DEFAULT_CONTENT_TYPE_HEADER "Content-Type: application/json; charset=utf-8\n"
#define INFO_ENDPOINT_PART "/info.0.json"
// every api endpoint
#define INFO_ENDPOINT API_BASE INFO_ENDPOINT_PART
#define START_SESSION_ENDPOINT API_BASE "/start_session.0.json"
#define LOGIN_ENDPOINT API_BASE_HTTPS "/login.0.json"
// for matching and manipulating authenticate requests through give up
#define AUTHENTICATE_ENDPOINT_PREFIX "/authenticate."
struct MemoryStruct {
char *memory;
size_t size;
};
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if(!ptr) {
/* out of memory! */
perror("realloc failed in WriteMemoryCallback");
return EXIT_FAILURE;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
return realsize;
}
struct saved_t {
char *session_id;
char *auth;
time_t auth_expiry;
} saved;
pthread_mutex_t saved_mutex = PTHREAD_MUTEX_INITIALIZER;
void save_vars() {
FILE *file = fopen(SAVED_FILE_NAME, "w+");
if(!file) {
perror("save_vars failed");
return;
}
pthread_mutex_lock(&saved_mutex);
fprintf(file, "%s\n%s\n%ld\n",
saved.session_id, saved.auth, saved.auth_expiry);
fclose(file);
pthread_mutex_unlock(&saved_mutex);
}
void load_vars() {
FILE *file = fopen(SAVED_FILE_NAME, "r");
if(!file) {
perror("load_vars failed");
return;
}
pthread_mutex_lock(&saved_mutex);
int result = fscanf(file, "%ms\n%ms\n%mld\n",
&saved.session_id, &saved.auth, &saved.auth_expiry);
if(result == EOF || result != 3) {
fprintf(stderr, "fscanf failed or read wrong amount of strings, saved was not loaded properly!");
saved.session_id = NULL;
saved.auth = NULL;
}
//fread(&saved, sizeof(saved), 1, file);
fclose(file);
pthread_mutex_unlock(&saved_mutex);
}
// reused for give up, defined in main
// give up shouldn't run before these are defined though
/*char *request_uri_env;
char *query_string;
int request_method_post_compare = 1;
CURL *curl;*/
struct request_supplemental_t {
char *request_uri_env;
char *query_string;
char *post_data;
int request_method_post_compare;
CURL *curl;
};
// called when giving up the request via the alt and trying to instead pass it unmodified
void give_up(struct request_supplemental_t *req) {
// free both of these at the end of the script
if(saved.session_id != NULL)
free(saved.session_id);
if(saved.auth != NULL)
free(saved.auth);
if(req->post_data != NULL)
free(req->post_data);
/*if(no_give_up) {
// return if marked as not giving up
return;
}*/
//puts("\ngiveUp()");
// start give up request
if(req->curl) {
curl_easy_cleanup(req->curl);
}
req->curl = curl_easy_init();
if(!req->curl) {
fprintf(stderr, "give up curl_easy_init failed\n");
return;
}
char *this_api_base = API_BASE;
// only use regular if is one of login, logout, authenticate
int authenticate_compare = strncmp(req->request_uri_env, AUTHENTICATE_ENDPOINT_PREFIX, strlen(AUTHENTICATE_ENDPOINT_PREFIX));
if(strncmp(req->request_uri_env, "/logi", 5) == 0
|| strncmp(req->request_uri_env, "/logo", 5) == 0
|| authenticate_compare == 0
) {
//puts("using https for this request");
this_api_base = API_BASE_HTTPS;
}
char *url = malloc(strlen(this_api_base)
+ strlen(req->request_uri_env)
+ 1
);
url[0] = '\0';
strcat(url, this_api_base);
strcat(url, req->request_uri_env);
struct MemoryStruct chunk;
// only store output in memory if it is authenticate request
if(authenticate_compare == 0) {
printf("authenticate compare = 0. request uri env: %s", req->request_uri_env);
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, (void *)&chunk);
}
curl_easy_setopt(req->curl, CURLOPT_URL, url);
free(url);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(req->curl, CURLOPT_POST, 1);
curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, req->query_string);
// return json for some reason
puts(DEFAULT_CONTENT_TYPE_HEADER);
CURLcode res = curl_easy_perform(req->curl);
if(res != CURLE_OK) {
fprintf(stderr, "give up curl_easy_perform failed: %s\n", curl_easy_strerror(res));
curl_easy_cleanup(req->curl);
if(authenticate_compare == 0)
free(chunk.memory);
// shortcut syntax
if(req->request_method_post_compare == 0)
free(req->query_string);
return;
}
// now hackily replace text if authenticate endpoint
//puts(request_uri_env);
if(authenticate_compare == 0) {
//puts("matched authenticate, replacing");
char string_replace[] = "um\":\"\"";
char *start_at = strstr(chunk.memory, string_replace);
if(start_at) {
//printf("strstr: %ld", start_at);
// cut string at beginning of start
*start_at = '\0';
//printf("start_at: %s", start_at);
// advance pointer to after string to replace
char *end_at = start_at + strlen(string_replace);
// should replace string sufficiently??
printf("%sum\":\"anime\"%s", chunk.memory, end_at);
} else {
fprintf(stderr, "no match found for %s in the authenticate response, whatever...\n", string_replace);
puts(chunk.memory);
}
free(chunk.memory);
}
curl_easy_cleanup(req->curl);
/*printf(
"todo for give up:\n"
"* POST request to %s%s"
"\n* here is the data: %s"
"\nthnks\n\n"
, API_BASE, request_uri_env, query_string);
*/
//fprintf(stderr, "giveUp()\n");
//require_once 'proxy-curl.php';
// free query string from post at the very end
if(req->request_method_post_compare == 0)
free(req->query_string);
}
int start_session_flow(struct request_supplemental_t *req, int *pending_save, int *start_session_attempts) {
(*start_session_attempts)++;
if(*start_session_attempts > 3) {
fprintf(stderr, "start session attempts exceeded\n");
return EXIT_FAILURE;
}
struct MemoryStruct chunk;
CURLcode res;
char *post_data2;
struct json_object *json;
struct json_object *data;
struct json_object *auth;
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(req->curl, CURLOPT_POST, 1);
// if no auth or if current time is more than auth expiry minus 24 hours
if(!saved.auth || strlen(saved.auth) == 0
|| !saved.auth_expiry
|| (saved.auth_expiry - 86400) < time(NULL)
) {
//puts("NO AUTH OR IT EXPIRED FETCHING NEW AUTH!!!!!! (login flow)");
// beginning login flow
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, (void *)&chunk);
curl_easy_setopt(req->curl, CURLOPT_URL, LOGIN_ENDPOINT);
post_data2 = malloc(strlen(AUTH_POST_DATA)
+ strlen(API_CREDENTIALS)
+ 1
);
post_data2[0] = '\0';
strcat(post_data2, AUTH_POST_DATA);
strcat(post_data2, API_CREDENTIALS);
curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, post_data2);
res = curl_easy_perform(req->curl);
free(post_data2);
if(res != CURLE_OK) {
fprintf(stderr, "login curl_easy_perform failed: %s\n", curl_easy_strerror(res));
free(chunk.memory);
return EXIT_FAILURE;
}
// print and free response
//puts(chunk.memory);
json = json_tokener_parse(chunk.memory);
struct json_object *expires;
if(!json
|| !json_object_object_get_ex(json, "data", &data)
|| json_object_get_type(data) == json_type_null
|| !json_object_object_get_ex(data, "expires", &expires)
|| json_object_get_type(expires) == json_type_null
|| !json_object_object_get_ex(data, "auth", &auth)
|| json_object_get_type(auth) == json_type_null
) {
fprintf(stderr, "login response is not json or doesn't have necessary data, here is the response: %s\n", chunk.memory);
json_object_put(json);
free(chunk.memory);
return EXIT_FAILURE;
}
free(chunk.memory);
//puts("handle auth and auth expiry");
//puts(json_object_get_string(auth));
if(saved.auth != NULL)
free(saved.auth);
saved.auth = strdup(json_object_get_string(auth));
// parse out date
char *expires_str = (char *)json_object_get_string(expires);
char format[] = "%Y-%m-%dT%H:%M:%S%z";
struct tm date;
if(!strptime(expires_str,
format, &date)) {
fprintf(stderr, "failed to parse auth expiry date, here it is: %s\n", expires_str);
// hack to just temporarily set auth expiry to one week in the future if it doesn't work
time_t adhoc_time = (time(NULL) + 604800);
saved.auth_expiry = adhoc_time;
/*json_object_put(json);
return EXIT_FAILURE;*/
}
saved.auth_expiry = mktime(&date);
json_object_put(json);
}
//puts("entering start session request");
// reinitialize chunk
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, (void *)&chunk);
curl_easy_setopt(req->curl, CURLOPT_URL, START_SESSION_ENDPOINT);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(req->curl, CURLOPT_POST, 1);
char auth_str[] = "&auth=";
char *auth_escaped = curl_easy_escape(req->curl, saved.auth,
strlen(saved.auth)
);
post_data2 = malloc(strlen(AUTH_POST_DATA)
+ strlen(auth_str)
+ strlen(auth_escaped)
+ 1
);
// initialize as empty string
post_data2[0] = '\0';
strcat(post_data2, AUTH_POST_DATA);
strcat(post_data2, auth_str);
strcat(post_data2, auth_escaped);
curl_free(auth_escaped);
curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, post_data2);
res = curl_easy_perform(req->curl);
free(post_data2);
if(res != CURLE_OK) {
fprintf(stderr, "start session curl_easy_perform failed: %s\n", curl_easy_strerror(res));
free(chunk.memory);
return EXIT_FAILURE;
}
// print and free response
//puts(chunk.memory);
json = json_tokener_parse(chunk.memory);
struct json_object *session_id;
//puts(json_object_to_json_string(json));
if(!json
|| !json_object_object_get_ex(json, "data", &data)
|| json_object_get_type(data) == json_type_null
|| !json_object_object_get_ex(data, "session_id", &session_id)
|| json_object_get_type(session_id) == json_type_null
) {
fprintf(stderr, "start session response is not json or doesn't have necessary data, here is the response: %s\n", chunk.memory);
json_object_put(json);
free(chunk.memory);
return EXIT_FAILURE;
}
free(chunk.memory);
struct json_object *user;
if(!json_object_object_get_ex(data, "user", &user)
|| json_object_get_type(user) == json_type_null) {
//puts("no user...... this is where you go restart");
if(saved.auth != NULL)
free(saved.auth);
// reset saved.auth and restart the flow so that it fetches again
saved.auth = NULL;
int again_result = start_session_flow(req, pending_save, start_session_attempts);
if(again_result != 0) {
json_object_put(json);
return again_result;
}
return EXIT_SUCCESS;
}
//struct json_object *auth;
if(json_object_object_get_ex(data, "auth", &auth)
&& json_object_get_type(auth) != json_type_null) {
//puts("auth is here");
if(saved.auth != NULL)
free(saved.auth);
saved.auth = strdup(json_object_get_string(auth));
}
//puts("handle session id, pend save");
//puts(json_object_get_string(session_id));
if(saved.session_id != NULL)
free(saved.session_id);
saved.session_id = strdup(json_object_get_string(session_id));
//printf("new session id: %s\n", saved.session_id);
*pending_save = 1;
json_object_put(json);
return EXIT_SUCCESS;
}
int perform_target_request(struct request_supplemental_t *req, int *pending_save, int *start_session_attempts) {
//puts("target request is being entered");
// make memory output chunk
struct MemoryStruct chunk;
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, (void *)&chunk);
// set cURL options
curl_easy_setopt(req->curl, CURLOPT_URL, INFO_ENDPOINT);
// append session id to post_data, 12 is the length of session id param string below
char session_id_str[] = "&session_id=";
char *start_at = strstr(req->post_data, session_id_str);
//puts(post_data);
if(start_at) {
// terminate post data at beginning of session id
*start_at = '\0';
}
req->post_data = realloc(req->post_data, strlen(req->post_data)
+ strlen(session_id_str)
+ strlen(saved.session_id)
+ 1
);
strcat(req->post_data, session_id_str);
strcat(req->post_data, saved.session_id);
//puts(post_data);
// set POST data
curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, req->post_data);
// perform request
CURLcode res = curl_easy_perform(req->curl);
// should be done with post data
// except this might be reused on next function call
//free(post_data);
// check for errors
if(res != CURLE_OK) {
fprintf(stderr, "target curl_easy_perform failed: %s\n", curl_easy_strerror(res));
free(chunk.memory);
return EXIT_FAILURE;
}
// print and free response
//puts(chunk.memory);
// search first 64 characters of chunk
char buffer[65];
strncpy(buffer, chunk.memory, 64);
// null terminate the string
buffer[64] = '\0';
// just check that there's no occurrence of `error":t` in first 64 characters
// if it DOES occur,
if(strstr(buffer, "error\":t")) {
// reset session_id and go back in case of either forbidden or bad session
// actually i don't know if forbidden actually means you're not authenticates
if(strstr(buffer, "code\":\"bad_session")) {
fprintf(stderr, "error code bad_session in target response\n");
if(saved.session_id != NULL)
free(saved.session_id);
saved.session_id = NULL;
// fetch session if it is bad
//puts("bad session, restarting session flow!!!!");
int start_result = start_session_flow(req, pending_save, start_session_attempts);
if(start_result != 0) {
free(chunk.memory);
return start_result;
}
// redo this routine too
free(chunk.memory);
return perform_target_request(req, pending_save, start_session_attempts);
} else {
fprintf(stderr, "error found in first 64 chars of json response: %s\n", chunk.memory);
free(chunk.memory);
return EXIT_SUCCESS;
}
}
// if the end was reached then success
// print content type
puts(DEFAULT_CONTENT_TYPE_HEADER);
puts(chunk.memory);
free(chunk.memory);
//free(post_data);
return EXIT_SUCCESS;
}
int main() {
//atexit(give_up);
char *request_uri_env = getenv("REQUEST_URI");
if(!request_uri_env) {
fprintf(stderr, "REQUEST_URI is not set\n");
//no_give_up = 1;
return EXIT_FAILURE;
}
//assert(request_uri_env && strcmp(request_uri_env, INFO_ENDPOINT_PART) == 0);
char *request_method = getenv("REQUEST_METHOD");
if(!request_method) {
fprintf(stderr, "REQUEST_METHOD is not set\n");
//no_give_up = 1;
return EXIT_FAILURE;
}
//char *query_string;
struct request_supplemental_t req = {
.request_uri_env = request_uri_env,
.request_method_post_compare = strcmp(request_method, "POST"),
};
if(strcmp(request_method, "GET") == 0) {
req.query_string = getenv("QUERY_STRING");
if(!req.query_string || strlen(req.query_string) == 0) {
// look for query string in request uri as a last resort
char *q_at = strchr(request_uri_env, '?');
if(q_at != NULL) {
// terminate string at separator to make two parts
// this makes request_uri_env cut off at this point though
*q_at = '\0';
// advance pointer, now q_at can be the query string
q_at++;
req.query_string = q_at;
} else {
fprintf(stderr, "QUERY_STRING is not set or is empty, and it's not hiding in request_uri, even though there is a GET request\n");
//no_give_up = 1;
return EXIT_FAILURE;
}
}
} else if(req.request_method_post_compare == 0) {
//char buffer[500];
// tried a buffer of 500 but some lengths exceeded
req.query_string = malloc(1000);
fgets(req.query_string, 1000, stdin);
// strip last newline
req.query_string[strcspn(req.query_string, "\n")] = 0;
//query_string = buffer;
} else {
fprintf(stderr, "unhandled REQUEST_METHOD: %s\n", request_method);
//no_give_up = 1;
free(req.query_string);
return EXIT_FAILURE;
}
//puts(query_string);
if(strncmp(request_uri_env, INFO_ENDPOINT_PART, strlen(INFO_ENDPOINT_PART)) != 0) {
fprintf(stderr, "REQUEST_URI is %s instead of what we want. passing through\n", request_uri_env);
give_up(&req);
return EXIT_FAILURE;
}
//char *post_data = malloc(strlen(query_string) + 1);
req.post_data = malloc(1);
req.post_data[0] = '\0';
int need_ampersand = 0;
int locale_ever_matched = 0;
// Work on a copy of query_string to preserve the original
char *query_string_copy = strdup(req.query_string);
// Tokenize the query string using the '&' character as the delimiter
char *token = strtok(query_string_copy, "&");
while(token) {
// Split the token into key and value using the '=' character
char *key = token;
char *value = strchr(token, '=');
if(!value) {
continue;
}
// Replace the '=' character with a null terminator to separate the key and value
// also this makes token useless now
*value = '\0';
// Move the pointer to the character after the = sign
value++;
// Check if the key matches one of the specified values
int fields_match = strcmp(key, "fields") == 0;
int locale_match = strcmp(key, "locale") == 0;
if(fields_match
|| locale_match
|| strcmp(key, "media_id") == 0
) {
// check fields to see if it matches what we want
if(fields_match
// must be media.stream_data for our purposes
&& !strstr(value, "media.stream_data")
) {
fprintf(stderr, "fields does not contain (first 32: %.32s), passing through\n", value);
free(query_string_copy);
give_up(&req);
return EXIT_FAILURE;
}
if(locale_match) {
locale_ever_matched = 1;
}
// Conditionally add & sign
if(need_ampersand) {
strcat(req.post_data, "&");
}
need_ampersand = 1;
req.post_data = realloc(req.post_data, strlen(req.post_data)
+ strlen(key)
+ strlen(value)
// also account for =
+ 2
// should be 1 if present
+ need_ampersand
);
// Append the key-value pair to the 'post_data' string
strcat(req.post_data, key);
strcat(req.post_data, "=");
strcat(req.post_data, value);
}
// Get the next token
token = strtok(NULL, "&");
}
// copy should not be needed
free(query_string_copy);
// load saved parameters
load_vars();
/*if(saved.session_id != NULL && saved.auth != NULL && saved.auth_expiry != NULL) {
*///printf("loaded vars, here: %s %s %ld\n", saved.session_id, saved.auth, saved.auth_expiry);
/*} else {
puts("saved vars are null noooooooon ooooooooooooo");
}*/
// initialize cURL
req.curl = curl_easy_init();
if(!req.curl) {
fprintf(stderr, "target curl_easy_init failed\n");
if(req.request_method_post_compare == 0)
free(req.query_string);
//no_give_up = 1;
if(saved.session_id != NULL)
free(saved.session_id);
if(saved.auth != NULL)
free(saved.auth);
return EXIT_FAILURE;
}
// no session id = restart auth process
//printf("checking session id for starting flow at the beginning: %s", saved.session_id);
int pending_save = 0;
int start_session_attempts = 0;
if(saved.session_id == NULL
|| strlen(saved.session_id) == 0) {
int start_result = start_session_flow(&req, &pending_save, &start_session_attempts);
// pass the start session flow result if it failed
// because if it did then it probably already showed an error
if(start_result != 0) {
give_up(&req);
return start_result;
}
}
// add default locale if it was not specified in the request
if(!locale_ever_matched) {
char locale_str[] = "&locale=";
req.post_data = realloc(req.post_data, strlen(req.post_data)
+ strlen(locale_str)
+ strlen(DEFAULT_LOCALE)
+ 1
);
strcat(req.post_data, locale_str);
strcat(req.post_data, DEFAULT_LOCALE);
}
curl_easy_setopt(req.curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(req.curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(req.curl, CURLOPT_POST, 1);
int target_result = perform_target_request(&req, &pending_save, &start_session_attempts);
if(target_result != 0) {
//curl_easy_cleanup(curl);
free(req.post_data);
give_up(&req);
return target_result;
}
//no_give_up = 1;
if(req.request_method_post_compare == 0)
free(req.query_string);
free(req.post_data);
//printf("after start session flow (status %d)\n", start_result);
// cleanup
curl_easy_cleanup(req.curl);
if(pending_save) {
save_vars();
//printf("saved vars, here: %s %s %ld\n", saved.session_id, saved.auth, saved.auth_expiry);
}
if(saved.session_id != NULL)
free(saved.session_id);
if(saved.auth != NULL)
free(saved.auth);
}
<?php
//define('API_BASE', 'https://api.crunchyroll.com');
// api.crunchyroll.com resolution, 'raw ip' cloudflare server to pass to
// this is used in my case cause my target device doesn't resolve this correctly
define('API_BASE', 'https://104.18.22.9');
define('API_HOST', 'api.crunchyroll.com');
// mitmproxy or busybox instance for testing
//define('API_BASE', 'http://127.0.0.1:8005');
//define('API_BASE', 'http://127.0.0.1:8004');
// information about the device included in auth requests
define('AUTH_POST_DATA', array(
// apple tvos cause that's what i'm accessing on anyway
'device_type' => 'com.crunchyroll.tvos',
'access_token' => 'b78a05ad1fd6717',
/*'device_type' => 'com.crunchyroll.windows.desktop',
'access_token' => 'LNDJgOit5yaRIWN',
*/
// hardcoded device id
'device_id' => 'just make a random uuid to put here'
));
// not necessary either but included just for fun
define('USER_AGENT', 'Crunchyroll/2.7.0 (com.crunchyroll.iphone; build:7; tvOS 14.7.0) Alamofire/5.4.4');
// miscallaneous data included in most requests
define('MISC_POST_DATA', array(
// not strictly required ever
'connectivity_type' => 'ethernet',
'locale' => 'enUS',
'version' => '2.7.0'
));
// crunchyroll credentials in question to pass through
define('API_CREDENTIALS', array(
// premium account, otherwise there is no point.
'account' => 'username here!!!!!!!',
'password' => 'password'
));
// hide errors, particularly about file_get_contents not working
// so that the output is still valid json in normal circumstances
ini_set('display_errors', 'Off');
// exception that's thrown when an error is seen in the api itself
// in order to differentiate e.g request and response errors from auth errors
class APIException extends Exception {
/*public $code;
public $message;
public function __construct($code, $message) {
$this->code = $code;
$this->message = $message;
}
public function getMessage() {
/*if(!$this->code) {
return $this->message;
}
return 'error code: ' . $this->code . ', message: ' . $this->message;
}*/
}
$http_response_header_global = array();
// wrapper function that makes a request to the api handling potential errors
function apiRequest($endpoint, $postData, $returnJSON = true) {
global $http_response_header_global;
$context = stream_context_create(array(
// because the site is currently being accessed via its ip address, the sni name and http host are being passed instead to indicate the hostname
'ssl' => array(
'verify_peer' => false,
'peer_name' => API_HOST
), 'http' => array(
'method' => 'POST',
'header' => "Host: " . API_HOST . "\r\n" . 'User-Agent: ' . USER_AGENT . "\r\n" . 'Content-Type: application/x-www-form-urlencoded',
// making http post data from post data array
'content' => http_build_query($postData)
)
));
try {
$response = file_get_contents(API_BASE . $endpoint, false, $context);
// set http headers to be available globally instead of only in the local scope to be used later
$http_response_header_global = $http_response_header;
} catch(Exception $e) {
// this should re-throw the exception while also just ending the function all in all
throw $e;
return false;
}
// if the socket cannot connect then it won't throw an exception apparently but it will be false so i am throwing an exception and returning false so code won't continue
if(!$response) {
throw new Exception('response is falsey');
return false;
}
//echo 'SHOULD NOT RUN WHEN THERE IS AN AROR!!!!!!!!!!!! ' . PHP_EOL;
if(!$returnJSON) {
$haystack = substr($response, 0, 64);
// just check that there's no occurrence of `error":t` in first 64 characters
// if it DOES occur,
if(strpos($haystack, 'error":t') !== false) {
if(strpos($haystack, 'code":"forbidden') !== false) {
// return error code 1 in the event of a forbidden response
throw new APIException('error code forbidden!!!!!!!! in response: ' . $response, 1);
}
else if(strpos($haystack, 'code":"bad_session') !== false) {
// forbidden or bad session
throw new APIException('error code bad_session. in response: ' . $response, 1);
} else {
throw new APIException('error found in first 64 chars of json response: ' . $response);
}
}
return $response;
} else {
$result = json_decode($response);
if($result->error) {
// throw an APIException defined earlier if crunchyroll's api indicates an error, the existence of the "code" and "message" properties are implied
throw new APIException('error code: ' . $result->code . ', message: ' . $result->message);
}
return $result;
}
}
// called when giving up the request via the alt and trying to instead pass it unmodified
function giveUp() {
/*$result = apiRequest('/info.0.json', array(
'session_id' => $saved['session_id'],
'fields' => 'media.stream_data',
'media_id' => $_POST['media_id'],
), false);*/
require_once 'proxy.php';
//die('give up');
exit();
}
// determine if to give up; if request uri isn't info
if($_SERVER['REQUEST_URI'] !== '/info.0.json' ||
// or if there's no post slash fields is not there
!array_key_exists('fields', $_POST) ||
// and finally, if fields is not media.stream_data
$_POST['fields'] !== 'media.stream_data') {
// give up now
giveUp();
}
// array where saved auth is stored just like in the file
$saved = array(
'session_id' => null,
'auth' => null,
'auth_expiry' => null
);
// will make a file name like /tmp/cronch-(device id)
$savedFileName = sys_get_temp_dir() . '/cronch-' . AUTH_POST_DATA['device_id'];
// this is intended to throw an error when the file doesn't exist
$savedFileContents = file_get_contents($savedFileName);
if($savedFileContents) {
// unserialize and load the file when it did load correctly
$saved = unserialize($savedFileContents);
}
// this is to be set true when the array is modified
// and this is read at the end and the file is saved according to this
$savedPendingSave = false;
//print_r($saved);
// login to get auth
// this is a last resort so if it fails then it just won't work
function apiLogin() {
global $saved, $savedPendingSave;
try {
$result = apiRequest('/login.0.json', array_merge(AUTH_POST_DATA, API_CREDENTIALS));
} catch(Throwable $e) {
// api exception here means that the credentials may have been incorrect
// and so in this case this will just not work
trigger_error($e);
giveUp();
return false;
}
$saved['auth'] = $result->data->auth;
// convert expiry from a format like this: 2022-05-06T18:30:28-07:00
$saved['auth_expiry'] = strtotime($result->data->expires);
$savedPendingSave = true;
//echo 'auth ' . $saved['auth'] . PHP_EOL;
return $result->data->auth;
}
// start session using auth
function apiStartSession() {
global $saved, $savedPendingSave;
try {
$result = apiRequest('/start_session.0.json', array_merge(AUTH_POST_DATA, array(
'auth' => $saved['auth']
)));
} catch(Throwable $e) {
throw $e;
return false;
}
// if user is null then the session begin did not work
if(!$result->data->user) {
throw new APIException('user was null in session response, auth is incorrect');
return false;
}
// make sure to save the auth returned by start_session if it was present
if($result->data->auth) {
$saved['auth'] = $result->data->auth;
// inferring that expires is there too
$saved['auth_expiry'] = strtotime($result->data->expires);
}
$saved['session_id'] = $result->data->session_id;
$savedPendingSave = true;
//echo 'session ' . $saved['session_id'] . PHP_EOL;
// so that a truey value is returned
return $result->data->session_id;
}
// counter for attempts to do target request so it doesn't do too many
$targetAttemptsCounter = 0;
// total goto limit counter that must not exceed 3 so that no infinite loop happens ideally
$gotoLimitCounter = 0;
startSession:
//echo 'it begins' . PHP_EOL;
if($gotoLimitCounter >= 3) {
giveUp();
}
// this is a counter to ensure this does not run more than three times and go in an infinite loop and Die
if($targetAttemptsCounter > 3) {
//echo 'attempts exceeded!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' . PHP_EOL;
giveUp();
}
if(!$saved['session_id']) {
// if no auth or if current time is more than auth expiry minus 24 hours
if(!$saved['auth'] || ($saved['auth_expiry'] - 86400) < time()) {
apiLogin();
}
//echo 'no saved session id, apiStartSession(), before:' . $saved['session_id'] . PHP_EOL;
try {
apiStartSession();
} catch(APIException $e) {
// also log in if start session has failed
//exit('dying after api exception session');
$saved['auth'] = null;
$gotoLimitCounter++;
goto startSession;
}
catch(Throwable $e) {
trigger_error($e);
giveUp();
}
/*if(!$startSessionResult) {
apiLogin();
}*/
//echo 'session id after: ' . $saved['session_id'] . PHP_EOL;
}
$targetAttemptsCounter++;
//echo 'do target request w session ' . $saved['session_id'] . PHP_EOL;
$postData = array(
// without locale it occasionally gives an error in german
'locale' => MISC_POST_DATA['locale'],
'session_id' => $saved['session_id'],
'fields' => $_POST['fields'],
'media_id' => $_POST['media_id']
);
// add these optional parameters (that i am not even handling) as a hack to Just Get It Working Already
/*if(array_key_exists('collection_id', $_POST)) {
$postData['collection_id'] = $_POST['collection_id'];
}
if(array_key_exists('series_id', $_POST)) {
$postData['series_id'] = $_POST['series_id'];
}
if(array_key_exists('media_id', $_POST)) {
$postData['media_id'] = $_POST['media_id'];
}*/
// locale has to be correct for different dubs to work apparently?
// i don't think this works actually lol but i'm including anyway
if(array_key_exists('locale', $_POST)) {
$postData['locale'] = $_POST['locale'];
}
try {
$result = apiRequest('/info.0.json', $postData, false);
// session was probably incorrect
} catch(APIException $e) {
// give up if error code is not forbidden, or 1 as was set earlier in the exception
if($e->getCode() !== 1) {
trigger_error($e);
giveUp();
}
// continuing if error code is forbidden
//trigger_error($e);
//echo $e;
//echo 'resetting session_id, goto startSession' . PHP_EOL;
$saved['session_id'] = null;
//exit('eror target');
$gotoLimitCounter++;
goto startSession;
} catch(Throwable $e) {
trigger_error($e);
giveUp();
}
// save at the very end of this cycle
if($savedPendingSave) {
file_put_contents($savedFileName, serialize($saved));
//echo 'saved'.PHP_EOL;
}
//print_r($http_response_header_global);
// go through http headers in the global scope that were defined by apiRequest
foreach($http_response_header_global as $header) {
//echo $header . PHP_EOL;
if(strtolower(substr($header, 0, 14)) === 'content-type: ') {
//echo 'this is it: ' . $header . PHP_EOL;
// set the content type header that was returned
header($header);
}
}
echo $result;
<?php
// This works best on Apache, and if you have enable_post_data_reading off.
// Set "php_flag enable_post_data_reading off" in your htaccess. It'll avoid having to parse multipart forms.
// Close the session right now, because it might make everything faster, and we don't know how long the response will last for.
//session_write_close();
// mute because proxy.php already defines these ( just forgive me for these the alternative was a redundant check if it was alread defined ok)
@define('API_BASE', 'https://api.crunchyroll.com');
@define('API_RESLOVED', 'api.crunchyroll.com:443:104.18.40.133');
/*
$headers = array();
foreach(getallheaders() as $name => $value) {
// seems that headers with no value get passed sometimes
// and upstreams return a bad request for no reason because of it
if($value === '') {
continue;
}
if($name === 'Connection' || $name === 'X-Https' || $name === 'Host') {
continue;
}
array_push($headers, $name . ': ' . $value . "\r\n");
}
*/
//$headers .= 'Host: ' . API_HOST . "\r\n";
// set up the curl options
$options = array(
//CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'identity',
CURLOPT_HEADERFUNCTION =>
function($curl, $header) {
if(substr($header, 0, 17) !== 'Transfer-Encoding') {
header($header);
}
return strlen($header);
},
CURLOPT_WRITEFUNCTION =>
function($curl, $data) {
echo str_replace('um":""', 'um":"anime"', $data);
ob_flush();
flush();
return strlen($data);
},
CURLOPT_SSL_VERIFYPEER => false,
//CURLOPT_RESOLVE => array(API_RESLOVED),
CURLOPT_CUSTOMREQUEST => $_SERVER['REQUEST_METHOD'],
//CURLOPT_HTTPHEADER => $headers
CURLOPT_POSTFIELDS => file_get_contents('php://input')
);
// initialize a new curl resource
$curl = curl_init(API_BASE . $_SERVER['REQUEST_URI']);
// set the curl options
curl_setopt_array($curl, $options);
// execute the request
$response = curl_exec($curl);
// check for errors
if(!$response) {
http_response_code(502);
header('Content-Type: text/plain');
//print_r(curl_getinfo($curl));
exit('curl: (' . curl_errno($curl) . ') ' . curl_error($curl));
}
// close the curl resource
curl_close($curl);
<?php
// This works best on Apache, and if you have enable_post_data_reading off.
// Set "php_flag enable_post_data_reading off" in your htaccess. It'll avoid having to parse multipart forms.
// Close the session right now, because it might make everything faster, and we don't know how long the response will last for.
session_write_close();
/*define('API_BASE', 'https://104.18.22.9');
define('API_HOST', 'api.crunchyroll.com');
*/
$headers = '';
foreach(getallheaders() as $name => $value) {
// seems that headers with no value get passed sometimes
// and upstreams return a bad request for no reason because of it
if($value === '') {
continue;
}
if($name === 'Connection' || $name === 'X-Https' || $name === 'Host') {
continue;
}
$headers .= $name . ': ' . $value . "\r\n";
}
$headers .= 'Host: ' . API_HOST . "\r\n";
$postData = '';
// read POST data
$input = fopen('php://input', 'r');
while(!feof($input)) {
$data = fgets($input, 1024);
if(empty($data) && (!empty($_POST) || !empty($_FILES))) {
// polyfill for if enable_post_data_reading is on, start parsing $_POST to a "raw" post data equivalent
// see what content type is there (there should be a content type at this point, if php is parsing post data)
// actually, i found out that only multipart/form-data should be parsed here?
// also see if it is long enough to have a boundary
if(substr($_SERVER['HTTP_CONTENT_TYPE'], 0, 19) === 'multipart/form-data' && strlen($_SERVER['HTTP_CONTENT_TYPE']) > 30) {
$boundary = '--' . substr($_SERVER['HTTP_CONTENT_TYPE'], 30);
foreach($_POST as $name => $value) {
// key/values...
$part = $boundary . "\r\n" . 'Content-Disposition: form-data; name="' . $name . '"' . "\r\n\r\n" . $value . "\r\n";
$postData .= $part;
}
// files
$out = fopen('php://stdout', 'w');
foreach($_FILES as $name => $value) {
$part = $boundary . "\r\n" . 'Content-Disposition: form-data; name="' . $name . '"; filename="' . $value['name'] . '"' . "\r\nContent-Type: " . $value['type'] . "\r\n\r\n";
$postData .= $part;
$file = fopen($value['tmp_name'], 'r');
// $file is sometimes falsey but i can't reproduce it
if($file) {
while(!feof($file)) {
$data = fgets($file, 1024);
$postData .= $data;
}
}
$postData .= "\r\n\r\n";
fclose($file);
}
$part = $boundary . '--';
$postData .= $part;
}
break;
}
$postData .= $data;
}
$context = stream_context_create(array(
'ssl' => array(
'verify_peer' => false,
'peer_name' => API_HOST
),
'http' => array(
// do not completely fail the request if there's an http error
'ignore_errors' => true,
'method' => $_SERVER['REQUEST_METHOD'],
'header' => $headers,
'content' => $postData
)
));
$buffered = file_get_contents(API_BASE . $_SERVER['REQUEST_URI'], false, $context);
// return error if file_get_contents fails
if(!$buffered) {
http_response_code(502);
header('Content-Type: text/plain');
exit('errno ' . $errno . ': ' . $errstr);
}
foreach($http_response_header as $header) {
header($header);
}
echo $buffered;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment