Skip to content

Instantly share code, notes, and snippets.

@larsloQ
Created October 19, 2022 11: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 larsloQ/a30471b3d42dbc7f72e69455ffe90719 to your computer and use it in GitHub Desktop.
Save larsloQ/a30471b3d42dbc7f72e69455ffe90719 to your computer and use it in GitHub Desktop.
SLIM-Framework based Micro-Proxy for accessing private Google Calendar URLS via JS
<?php
/**
* Background:
* A website I made wanted to show the pricing and availablity of some houses the offer for rent.
* they managed these data via simple google calendars, one for bookings/availabiliy and
* one where they enter prices for each day via recurring events a couple of weeks in advance.
*
* since google private calendar urls can not be access via JS (due to CORS)
* We need a proxy where we can controll CORS-Headers.
* Also we do not want to expose the private urls.
* So we needed a proxy.
*
* This is a typical https://www.slimframework.com/ kind of mirco-backend / micro-proxy
* preventing cors and returning ics / iCalendar data as strings
* with the following composer.json:
* {
"require": {
"slim/slim": "4.*",
"slim/psr7": "^1.5",
"guzzlehttp/psr7": "^2",
"selective/basepath": "^2.1",
"guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^2.8"
}
}
*
*/
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Selective\BasePath\BasePathMiddleware;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request as G7Req;
use Slim\Exception\HttpNotFoundException;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Set the base path to run the app in a subdirectory.
// This path is used in urlFor().
$app->add(new BasePathMiddleware($app));
/* print errors, log them, with details */
// $app->addErrorMiddleware( true, true, true );
/* for production: no display of errors */
$app->addErrorMiddleware(false, true, true);
$app->get(
'/',
function (Request $request, Response $response, $args) {
$response->getBody()->write('Hello world! ');
return $response;
}
);
$logger_settings = array(
'timezone' => 'UTC',
'log_method_not_allowed' => true,
'log_404' => true,
'path' => '../logs/error.log',
'name' => 'error',
);
// calendar_map.php
/*
* private urls of goggle calendar
* you can access them via route /{first_key}/{second_key}/ (see index.php)
*/
/**
*configure your private calendar urls there
*can be accessed via route house_1/bookings (see below)
*/
$calendar_map = array(
'house_1' => array(
'bookings' => '{YOUR_PRIVATE_GOOGLE_OR_OTHER_ICS_ICAL_CALENDAR_URL}',
'prices' => '{YOUR_PRIVATE_GOOGLE_OR_OTHER_ICS_ICAL_CALENDAR_URL}',
'third_calendar' => '{YOUR_PRIVATE_GOOGLE_OR_OTHER_ICS_ICAL_CALENDAR_URL}',
),
);
/*
* configure CORS
* when accessing this via browser/JS you need to set allowed_domain to the domain your JS runs at
* because of CORS
*/
$allowed_domain = 'http://localhost';
/**
* handling errors for this app. DRY
*
* @param Response $response The response
* @param array $error The error, gets logged, gets json_encode
* @param bool $with_calendar The with calendard, use our "Error.ics" calendar to return to show message inside of calendar
* @param int $http_status The http status, responding with 200 and having '$with_calendar' true makes frontend think that things went fine, so we can show the error message inside calendar
* @param bool $log log?
*
* @return <type> ( description_of_the_return_value )
*/
function handle_error(Response $response, array $error, bool $with_calendar = true, int $http_status = 200, bool $log = true)
{
if ($log) {
global $logger_settings;
$logger = new App\Services\LoggerFactory($logger_settings);
$logger->getLogger()->error(json_encode($error));
}
/* wrong key, return error calendar */
$error_content = file_get_contents(__DIR__ . '/ERROR.ics');
$response->getBody()->write($error_content);
return $response->withStatus($http_status);
}
/**
* access private calendar urls and return ics data as string
* house_key string must match one of first-level keys in config/calendar_map.php / $calendar_map
* cal_key string must match one of second-level keys in config/calendar_map.php / $calendar_map
*/
$app->get(
'/{house_key}/{cal_key}',
function (Request $request, Response $response, $args) use ($calendar_map) {
$urls = $calendar_map[ $args['house_key'] ];
$ical_url = $urls[ $args['cal_key'] ];
$error = array();
if (empty($urls)) {
$error = array(
'type' => 'wrong house key',
'wrong_key' => filter_var($args['house_key'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
);
}
$ical_url = $urls[ $args['cal_key'] ];
if (empty($ical_url)) {
$error = array(
'type' => 'wrong calendar key',
'wrong_key' => filter_var($args['cal_key'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'house' => filter_var($args['house_key'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
);
}
if (! empty($error)) {
return handle_error($response, $error, true, 200);
}
try {
$client = new GuzzleHttp\Client();
$res = $client->request(
'GET',
$ical_url,
array(
'headers' => array(),
'http_errors' => false, // do not FATAL ERROR on http errors
)
);
$calendar_ics = $res->getBody()->getContents();
$response->getBody()->write($calendar_ics);
return $response;
} catch (Exception $e) {
$error = array(
'type' => 'http-error',
'error' => $e->getMessage(),
'request' => $ical_url,
);
return handle_error($response, $error, false, 400, true);
}
}
);
/**
* CORS STUFF, JS PREFLIGHT, OPTIONS REQUEST
* see https://www.slimframework.com/docs/v4/cookbook/enable-cors.html
*/
$app->options(
'/{routes:.+}',
function ($request, $response, $args) {
return $response;
}
);
$app->add(
function ($request, $handler) use ($allowed_domain) {
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', $allowed_domain)
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
}
);
/**
* Catch-all route to serve a 404 Not Found page if none of the routes match
* NOTE: make sure this route is defined last
*/
$app->map(
array( 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ),
'/{routes:.+}',
function ($request, $response) {
throw new HttpNotFoundException($request);
}
);
$app->run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment