Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callbacks #26

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions proxy/callback.functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

function civiproxy_callback_validate_request_method($expected, $actual){
if(is_array($expected) && in_array($actual, $expected)){
return;
}
if(is_string($expected) && $expected == $actual){
return;
}
civiproxy_http_error("Invalid request method.", 405);
}

function civiproxy_callback_validate_content_type($expected, $actual){
if($expected != $actual){
civiproxy_http_error("Forbidden content type.", 403);
}
}

function civiproxy_callback_validate_body($expected, $actual, $content_type){
switch ($content_type) {
case 'application/json':
civiproxy_callback_validate_body_json($expected, $actual);
break;
case 'application/x-www-form-urlencoded':
civiproxy_callback_validate_body_xwwwformurlencoded($expected, $actual);
break;
default:
civiproxy_http_error("Forbidden content type (expecting {$expected}).", 403);
}
}

function civiproxy_callback_validate_body_json($expected, $actual) {
//TODO
}

function civiproxy_callback_validate_body_xwwwformurlencoded($expected, $actual) {
//TODO
}

// For now, I have written this 'placeholder' method to pass on post requests.
// Sparkpost says that it works OK. Might be a good idea to refactor/improve
// civiproxy_redirect() instead/as well.
function civiproxy_callback_redirect($target_path, $method) {
switch ($method) {
case 'POST':
civiproxy_callback_redirect_post($target_path);
break;
case 'GET':
civiproxy_callback_redirect_get($target_path);
break;
}
exit;
}

// Change the URL, forward the body. Respond with the response
function civiproxy_callback_redirect_post($target_path) {
global $target_civicrm;
$target_url = "$target_civicrm/{$target_path}";

$ch = curl_init();
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input'));
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$response = curl_exec($ch);
if (curl_error($ch)){
civiproxy_http_error("CURL error (" . curl_errno($ch) . ") ".curl_error($ch) , 501);
}

// I think that most callbacks just want a response code
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));

// But some might be interested in the response.
echo $response;
}

// Change the URL, forward the body. Respond with the response
function civiproxy_callback_redirect_get($target_path) {
global $target_civicrm;
$target_url = "$target_civicrm/{$target_path}";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$response = curl_exec($ch);
if (curl_error($ch)){
civiproxy_http_error("CURL error (" . curl_errno($ch) . ") ".curl_error($ch) , 501);
}

// I think that most callbacks just want a response code
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));

// But some might be interested in the response.
echo $response;
}
72 changes: 72 additions & 0 deletions proxy/callback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
// Handles callback URLs as follows:
// 1. Validates callback
// 2. Passes to civicrm if the payload passes validation
// 2. Returns an appropriate response (an HTML code)
// Note: valid callbacks should be defined as elements of a $callbacks variable
// in config.php.
// Callback URL should be configured in the service as follows:
// "{$proxy_base}/callback.php?source={$callbacks[$key]}&secret={$callbacks[$key]['secret']}"
// where $key is a key defined in the $callbacks variable in config.php.

require_once "config.php";
require_once "proxy.php";
require_once "callback.functions.php";

civiproxy_security_check('callback');

if (!isset($callbacks_enabled) || $callbacks_enabled !==true){
civiproxy_http_error("Feature disabled", 403);
}

// Check that this callback has been defined
parse_str($_SERVER['QUERY_STRING'], $query_params);
if(isset($callbacks[$query_params['source']])){
//Retrieve definition from config
$definition = $callbacks[$query_params['source']];
}else{
civiproxy_http_error("Undefined callback", 403);
}

// Check that a secret has been defined
if(!isset($definition['secret'])){
civiproxy_http_error("No secret defined for this callback", 501);
}

// Check secret has been sent
if(!isset($query_params['secret'])){
civiproxy_http_error("Secret missing from query parameters", 403);
}

// Check secret
if(!isset($query_params['secret']) || $definition['secret'] !== $query_params['secret'] ){
civiproxy_http_error("Invalid secret", 403);
}

// Check this is a supported request method
if(!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST'])){
civiproxy_http_error("Unsupported request method", 501);
}

// If a request method has been defined, validate it
if(isset($definition['request_method'])){
civiproxy_callback_validate_request_method($definition['request_method'], $_SERVER['REQUEST_METHOD']);
}

// Check this is a supported content type
if(!in_array($_SERVER['CONTENT_TYPE'], ['application/json', 'application/x-www-form-urlencoded'])){
civiproxy_http_error("Unsupported content type", 501);
}

// If a content type has been defined, validate it
if(isset($definition['content_type'])){
civiproxy_callback_validate_content_type($definition['content_type'], $_SERVER['CONTENT_TYPE']);
}

// TODO? implement body validation
if(isset($validator['body'])){
civiproxy_callback_validate_body($validator['body'], file_get_contents("php://input"), $_SERVER['CONTENT_TYPE']);
}

// We have passed all the validators, forward the request
civiproxy_callback_redirect($definition['target_path'], $_SERVER['REQUEST_METHOD']);
11 changes: 11 additions & 0 deletions proxy/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,14 @@
),
),
);

$callbacks_enabled = false;

$callbacks = [
'sparkpost' => [
'secret' => '85c573b980c3c248f083f9ca6a175659',
'request_method' => 'POST', // single value or array
'content_type' => 'application/json',
'target_path' => 'civicrm/sparkpost/callback'
]
];