<?php require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/sql.inc.php"); require_once($INC . "/prof.inc.php"); $rg_wh_error = ""; function rg_wh_set_error($str) { global $rg_wh_error; $rg_wh_error = $str; rg_log($str); } function rg_wh_error() { global $rg_wh_error; return $rg_wh_error; } $rg_wh_functions = array( 10000 => 'rg_wh_send', 10001 => 'rg_wh_send_one' ); rg_event_register_functions($rg_wh_functions); /* * Helper for rg_wh_send */ function rg_wh_send_one($db, $event) { rg_prof_start('wh_send_helper'); $wh = &$event['wh']; $info = &$wh['info']; rg_log_ml('wh_send_one: event: ' . print_r($event, TRUE)); if ($event['debug'] == 1) rg_log_ml('XXX DEBUG: wh[data]=' . print_r($wh['data'], TRUE)); $headers = array(); while (!empty($info['key'])) { if ($info['type'] == 0) break; $headers[] = 'X-RocketGit-Signature: ' . hash_hmac('sha512', $wh['data'], $info['key']); break; } $c = curl_init($info['url']); curl_setopt($c, CURLOPT_POST, 1); curl_setopt($c, CURLOPT_POSTFIELDS, $wh['data']); curl_setopt($c, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($c, CURLOPT_FOLLOWLOCATION, 0); // TODO: really? curl_setopt($c, CURLOPT_HEADER, 1); curl_setopt($c, CURLOPT_HTTPHEADER, $headers); curl_setopt($c, CURLOPT_USERAGENT, 'RocketGit WebHook'); curl_setopt($c, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt($c, CURLOPT_ENCODING, ''); // => use all methods curl_setopt($c, CURLOPT_VERBOSE, TRUE); curl_setopt($c, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($c, CURLOPT_CERTINFO, TRUE); // Else, a second wh, without client cert can use the connection: curl_setopt($c, CURLOPT_FORBID_REUSE, TRUE); $err = @fopen('php://temp', 'w'); if ($err !== FALSE) curl_setopt($c, CURLOPT_STDERR, $err); if (strchr($info['flags'], 'I')) curl_setopt($c, CURLOPT_SSL_VERIFYPEER, FALSE); if (strchr($info['flags'], 'H')) curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 0); // TODO verify 0 is a good value (default 2) $ret = FALSE; $cert_file = FALSE; $ca_file = FALSE; while (1) { $xid = rg_id(8); if (!empty($info['client_cert'])) { rg_log('DEBUG: will provide client cert...'); $f = 'wh-' . $event['ui']['uid'] . '-' . $wh['id'] . '-client-' . $xid; $cert_file = rg_tmp_file($f, $info['client_cert']); if ($cert_file === FALSE) break; curl_setopt($c, CURLOPT_SSLCERT, $cert_file); } else { rg_log('DEBUG: will NOT provide client cert...'); } if (!empty($info['client_ca_cert'])) { $f = 'wh-' . $event['ui']['uid'] . '-' . $wh['id'] . '-ca-' . $xid; $ca_file = rg_tmp_file($f, $info['client_ca_cert']); if ($ca_file === FALSE) break; curl_setopt($c, CURLOPT_CAINFO, $ca_file); } $r = curl_exec($c); if ($err !== FALSE) { rewind($err); $xerr = @fread($err, 16 * 4096); fclose($err); rg_log_ml($xerr); } if ($r === FALSE) { rg_log('Cannot execute curl: ' . curl_error($c)); $_info = curl_getinfo($c); rg_log_ml('Debug: ' . print_r($_info, TRUE)); if ($event['debug'] == 1) rg_cache_set('DEBUG::' . $event['ui']['uid'] . '::webhooks::' . $info['opaque'] . '::' . $wh['id'], 'BAD', RG_SOCKET_NO_WAIT); break; } rg_log_ml('Answer: ' . print_r($r, TRUE)); if ($event['debug'] == 1) rg_cache_set('DEBUG::' . $event['ui']['uid'] . '::webhooks::' . $info['opaque'] . '::' . $wh['id'], 'OK', RG_SOCKET_NO_WAIT); $ret = array(); break; } curl_close($c); if ($cert_file !== FALSE) @unlink($cert_file); if ($ca_file !== FALSE) @unlink($ca_file); rg_prof_end('wh_send_helper'); return $ret; } /* * Generic function which will be called when a webhook must be posted */ function rg_wh_send($db, $event) { rg_prof_start('wh_send'); rg_log_ml('wh_send: event: ' . print_r($event, TRUE)); // First, get the list of hooks $r = rg_wh_list($db, $event['ui']['uid']); if ($r['ok'] != 1) return FALSE; // Filter them by repo_id $real_list = array(); foreach ($r['list'] as $id => $info) { if (($info['repo_id'] != 0) && ($event['ri']['repo_id'] != $info['repo_id'])) { rg_log('hook is not for this repo'); continue; } // If the web hook does not contain our type, skip it if (!strchr($info['events'], $event['wh_event'])) { rg_log($event['wh_event'] . ' is not present in ' . $info['events']); continue; } $real_list[] = $id; } // Something to do? if (empty($real_list)) return array(); $cache = array(); $wh = array(); $ret = array(); foreach ($real_list as $id) { $wh['info'] = $r['list'][$id]; $wh['id'] = $id; $type = $wh['info']['type']; if (!isset($cache[$type])) { switch ($type) { case 0: // http post $cache[$type] = &$event['wh_data']; break; case 1: // php serialize $cache[$type] = serialize($event['wh_data']); break; default: rg_log('Unknown type ' . $type . '!'); $cache[$type] = ''; break; } } $wh['data'] = $cache[$type]; $x = $event; $x['category'] = 10001; $x['wh'] = $wh; $ret[] = $x; } rg_prof_end('wh_send'); return $ret; } $rg_wh_types = array( 0 => 'HTTP (application/x-www-form-urlencoded)', 1 => 'PHP serialize' ); /* * Transforms a type into HTML select */ function rg_wh_select_type($type) { global $rg_wh_types; $ret = '<select name="wh::type" id="type">'; foreach ($rg_wh_types as $_type => $name) { $add = ''; if ($_type == $type) $add = ' selected'; $ret .= '<option value="' . $_type . '"' . $add . '>' . $name . '</option>' . "\n"; } $ret .= '</select>' . "\n"; return $ret; } /* * Returns type text based on id */ function rg_wh_type($type) { global $rg_wh_types; foreach ($rg_wh_types as $_type => $name) if ($_type == $type) return $name; } $rg_wh_events = array( 'C' => 'Create repository', 'P' => 'Push', 'B' => 'Create branch' ); /* * Generates event list as html */ function rg_wh_check_events($events) { global $rg_wh_events; $ret = '<fieldset>'; $ret .= '<legend>Select trigger events</legend>'; $br = ''; foreach ($rg_wh_events as $id => $name) { $add = ''; if (strchr($events, $id)) $add = ' checked="checked"'; $ret .= $br . '<input type="checkbox" name="wh::events[' . $id . ']"' . ' id="events-' . $id . '"' . $add . ' />' . "\n" . '<label for="events-' . $id . '">' . $name . '</label>'; $br = '<br />' . "\n"; } $ret .= '</fieldset>' . "\n"; return $ret; } /* * Generates an events list as text */ function rg_wh_events($events) { global $rg_wh_events; $a = array(); foreach ($rg_wh_events as $id => $name) { if (strchr($events, $id)) $a[] = $name; } return implode(', ', $a); } $rg_wh_flags = array( 'I' => 'Do not verify the certificate', 'H' => 'Do not verify the hostname' ); /* * Generates flags list */ function rg_wh_check_flags($flags) { global $rg_wh_flags; $ret = '<fieldset>'; $ret .= '<legend>Flags</legend>'; $br = ''; foreach ($rg_wh_flags as $id => $name) { $add = ''; if (strchr($flags, $id)) $add = ' checked="checked"'; $ret .= $br . '<input type="checkbox" name="wh::flags[' . $id . ']"' . ' id="flags-' . $id . '"' . $add . ' />' . "\n" . '<label for="flags-' . $id . '">' . $name . '</label>'; $br = '<br />' . "\n"; } $ret .= '</fieldset>' . "\n"; return $ret; } /* * Generates a flags list as text */ function rg_wh_flags($flags) { global $rg_wh_flags; $a = array(); foreach ($rg_wh_flags as $id => $name) { if (strchr($flags, $id)) $a[] = $name; } return implode(', ', $a); } /* * Some cosmetics applied to a webhook */ function rg_wh_cosmetic(&$list) { foreach ($list as $id => &$row) { if (isset($row['itime'])) $row['itime_nice'] = gmdate('Y-m-d H:i', $row['itime']); if (isset($row['description'])) $row['HTML:description_nice'] = nl2br(rg_xss_safe($row['description'])); if (isset($row['type'])) $row['type_text'] = rg_wh_type($row['type']); if (isset($row['events'])) $row['events_text'] = rg_wh_events($row['events']); if (isset($row['flags'])) $row['flags_text'] = rg_wh_flags($row['flags']); if (isset($row['client_cert'])) $row['HTML:client_cert_short'] = empty($row['client_cert']) ? '' : nl2br(rg_xss_safe(substr($row['client_cert'], 0, 32))) . '...'; if (isset($row['client_ca_cert'])) $row['HTML:client_ca_cert_short'] = empty($row['client_ca_cert']) ? '' : nl2br(rg_xss_safe(substr($row['client_ca_cert'], 0, 32))) . '...'; } } /* * Returns a list of webhooks associated with a user * @repo_id may be 0 => hooks installed on user account */ function rg_wh_list($db, $uid) { rg_prof_start('wh_list'); rg_log_enter('wh_list'); $ret = array('ok' => 0, 'list' => array()); while (1) { $key = 'user' . '::' . $uid . '::' . 'wh'; $r = rg_cache_get($key); if ($r !== FALSE) { $ret['list'] = $r; $ret['ok'] = 1; break; } $params = array('uid' => $uid); $sql = 'SELECT * FROM webhooks' . ' WHERE uid = @@uid@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot load data'); break; } while (($row = rg_sql_fetch_array($res))) { $id = $row['id']; $ret['list'][$id] = $row; } rg_sql_free_result($res); rg_cache_set($key, $ret['list'], RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('wh_list'); return $ret; } /* * Adds/edits a webhook */ function rg_wh_add($db, $uid, $data) { rg_prof_start('wh_add'); rg_log_enter('wh_add'); $ret = array('ok' => 0); while (1) { $params = $data; $params['uid'] = $uid; $params['itime'] = time(); if ($data['id'] == 0) $sql = 'INSERT INTO webhooks (uid, repo_id, itime, events' . ', url, client_cert, client_ca_cert, flags' . ', add_ip, description, key, opaque)' . ' VALUES (@@uid@@, @@repo_id@@, @@itime@@' . ', @@events@@, @@url@@, @@client_cert@@' . ', @@client_ca_cert@@, @@flags@@, @@add_ip@@' . ', @@description@@, @@key@@, @@opaque@@)' . ' RETURNING id'; else $sql = 'UPDATE webhooks' . ' SET events = @@events@@' . ', url = @@url@@' . ', client_cert = @@client_cert@@' . ', client_ca_cert = @@client_ca_cert@@' . ', flags = @@flags@@' . ', type = @@type@@' . ', description = @@description@@' . ', key = @@key@@' . ', opaque = @@opaque@@' . ' WHERE uid = @@uid@@' . ' AND id = @@id@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot insert/update data'); break; } if ($data['id'] == 0) $row = rg_sql_fetch_array($res); rg_sql_free_result($res); if ($data['id'] == 0) $params['id'] = $row['id']; $key = 'user' . '::' . $uid . '::' . 'wh' . '::' . $params['id']; rg_cache_set($key, $params, RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('wh_add'); return $ret; } /* * Removes a list of webhooks */ function rg_wh_remove($db, $uid, $list) { rg_prof_start('wh_remove'); rg_log_enter('wh_remove'); $ret = array('ok' => 0); while (1) { if (empty($list)) { rg_wh_set_error('you did not select anything'); break; } $my_list = array(); foreach ($list as $id => $junk) $my_list[] = sprintf("%u", $id); $params = array('uid' => $uid); $sql_list = implode(', ', $my_list); $sql = 'DELETE FROM webhooks' . ' WHERE uid = @@uid@@' . ' AND id IN (' . $sql_list . ')'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_wh_set_error('cannot remove webhooks'); break; } rg_sql_free_result($res); foreach ($my_list as $junk => $id) { $key = 'user' . '::' . $uid . '::' . 'wh' . '::' . $id; rg_cache_unset($key, RG_SOCKET_NO_WAIT); } $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('wh_remove'); return $ret; } /* * High level function to list the webhooks */ function rg_wh_list_high_level($db, $rg, $paras) { rg_prof_start('wh_list_high_level'); rg_log_enter('wh_list_high_level'); $ret = ''; $errmsg = array(); $delete = rg_var_uint('delete'); while ($delete == 1) { if (!rg_valid_referer()) { $errmsg[] = 'invalid referer; try again'; break; } if (!rg_token_valid($db, $rg, 'wh_list', FALSE)) { $errmsg[] = 'invalid token; try again.'; break; } $list = rg_var_str("delete_list"); $r = rg_wh_remove($db, $rg['login_ui']['uid'], $list); if ($r['ok'] !== 1) { $errmsg[] = 'cannot delete: ' . rg_wh_error(); break; } $ret .= rg_template('user/settings/wh/delete_ok.html', $rg, TRUE /*xss*/); break; } $r = rg_wh_list($db, $rg['login_ui']['uid']); if ($r['ok'] !== 1) { $rg['errmsg'] = rg_wh_error(); $ret .= rg_template('user/settings/wh/list_err.html', $rg, TRUE /*xss*/); } else { rg_wh_cosmetic($r['list']); //rg_log_ml("DEBUG: r[list]: " . print_r($r['list'], TRUE)); $rg['rg_form_token'] = rg_token_get($db, $rg, 'wh_list'); $rg['HTML:errmsg'] = rg_template_errmsg($errmsg); $ret .= rg_template_table('user/settings/wh/list', $r['list'], $rg); } rg_log_exit(); rg_prof_end('wh_list_high_level'); return $ret; } /* * High level function to add/edit a web hook */ function rg_wh_add_high_level($db, $rg, $paras) { rg_prof_start('wh_add_high_level'); rg_log_enter('wh_add_high_level'); $ret = ''; $errmsg = array(); $show_form = TRUE; $rg['wh'] = array(); // We need the id in any case $rg['wh']['id'] = rg_var_str('wh::id'); $add = rg_var_uint('add'); while ($add == 1) { $rg['wh']['repo_id'] = rg_var_uint('wh::repo_id'); $rg['wh']['itime'] = time(); $rg['wh']['events'] = rg_var_a2s('wh::events'); // TODO $rg['wh']['url'] = rg_var_str('wh::url'); $rg['wh']['type'] = rg_var_uint('wh::type'); $rg['wh']['client_cert'] = trim(rg_var_str('wh::client_cert')); $rg['wh']['client_ca_cert'] = trim(rg_var_str('wh::client_ca_cert')); $rg['wh']['flags'] = rg_var_a2s('wh::flags'); $rg['wh']['add_ip'] = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; $rg['wh']['description'] = rg_var_str('wh::description'); $rg['wh']['opaque'] = rg_var_str('wh::opaque'); $rg['wh']['key'] = rg_var_str('wh::key'); // TODO: validate here the paras if ((strncasecmp($rg['wh']['url'], "http", 4) != 0) || (strncasecmp($rg['wh']['url'], "https", 5) != 0)) { $errmsg[] = 'invalid protocol; only http and https supported now'; break; } if (!rg_valid_referer()) { $errmsg[] = 'invalid referer; try again'; break; } if (!rg_token_valid($db, $rg, 'wh_add', FALSE)) { $errmsg[] = 'invalid token; try again.'; break; } $r = rg_wh_add($db, $rg['login_ui']['uid'], $rg['wh']); if ($r['ok'] !== 1) { $errmsg[] = rg_wh_error(); break; } $ret .= rg_template('user/settings/wh/edit_ok.html', $rg, TRUE /*xss*/); $show_form = FALSE; break; } if ($show_form) { // defaults if ($add == 0) { // TODO: if edit, load data based on id if ($rg['wh']['id'] > 0) { rg_log('TODO: edit is not yet implemented'); } else { // here is clear an add $rg['wh']['id'] = 0; $rg['wh']['events'] = ''; $rg['wh']['url'] = ''; $rg['wh']['type'] = 0; $rg['wh']['client_cert'] = ''; $rg['wh']['client_ca_cert'] = ''; $rg['wh']['flags'] = ''; $rg['wh']['description'] = ''; $rg['wh']['opaque'] = ''; $rg['wh']['key'] = ''; } } $rg['HTML:errmsg'] = rg_template_errmsg($errmsg); $rg['HTML:check_flags'] = rg_wh_check_flags($rg['wh']['flags']); $rg['HTML:check_events'] = rg_wh_check_events($rg['wh']['events']); $rg['HTML:select_type'] = rg_wh_select_type($rg['wh']['type']); $rg['rg_form_token'] = rg_token_get($db, $rg, 'wh_add'); $ret .= rg_template('user/settings/wh/add_edit.html', $rg, TRUE /*xss*/); } rg_log_exit(); rg_prof_end('wh_add_high_level'); return $ret; } /* * Main HL function for webhooks */ function rg_wh_high_level($db, $rg, $paras) { rg_prof_start('wh_high_level'); rg_log_enter('wh_high_level'); $ret = ''; $op = empty($paras) ? 'list' : array_shift($paras); $rg['menu']['wh'][$op] = 1; rg_log("DEBUG: op=$op"); $ret .= rg_template('user/settings/wh/menu.html', $rg, TRUE /*xss*/); switch ($op) { case 'add': $ret .= rg_wh_add_high_level($db, $rg, $paras); break; default: $ret .= rg_wh_list_high_level($db, $rg, $paras); break; } rg_log_exit(); rg_prof_end('wh_high_level'); return $ret; } ?>