Skip to content

Instantly share code, notes, and snippets.

@furyutei
Last active April 22, 2020 01:03
Show Gist options
  • Save furyutei/3f459e985ea9213a9c27283c0f850888 to your computer and use it in GitHub Desktop.
Save furyutei/3f459e985ea9213a9c27283c0f850888 to your computer and use it in GitHub Desktop.
GitHubからのWebhookにより自サーバー上の配信用ユーザースクリプト(*.user.js)を更新するPHPスクリプトサンプル

GitHubのWebフックをCentOS7上のApacheで受ける場合の例

GitHubからのWebhookによりサーバー(CentOS7+Apache)上の配信用ユーザースクリプト(*.user.js)を更新するための、PHPスクリプトのサンプル。
※ GitHub の Raw リンクから取得した場合、*.js のContent-Type が text/plain になるため、これを application/javascript で配信するための方策だったが、現在ではGitHub Pagesを使うとサーバーを用意する必要もなく簡便に出来る。

サンプルコード

  1. update_userjs-sample.phpGitHub からの Webhooks を受けて、自分のサーバー上ディレクトリ下(例では https://<ドメイン名>/userjs)の配信用ユーザースクリプトを更新するサンプル
  2. .htaccess: 配信用ユーザースクリプト(*.user.js)を置くディレクトリ(<DOCUMENT_ROOT>/userjs)に配置する .htaccess の例(HTTP Response Header 指定用)

トラブルシューティング

  1. SELinuxへの対策

資料

  1. ユーザースクリプト(*.user.js)取得時のHTTP Response Header の違い
<?php
/*
// [GitHub](https://github.com/) からの [Webhooks](https://developer.github.com/webhooks/) を受けて、
// 自分のサーバー上ディレクトリ下(例では https://<ドメイン名>/userjs)の配信用ユーザースクリプトを更新するサンプル
*/
// === User settings {
date_default_timezone_set( 'Asia/Tokyo' );
const DEBUG_FLAG = TRUE; // ■デバッグフラグ(※動作確認が済んだら FALSE に変えること)
const SECRET_KEY = '<your-secret-key>'; // ■シークレットキー(※GitHub側 の Secret にも同じものを登録すること)
$LOG_FILE = dirname( __FILE__ ) . '/log/github.log';
$USERJS_PATH_MAP = array(
//■ レジストリ内のユーザースクリプトの場所を登録
//'<user-name>/<registory-name>' => array(
// 'path/to/userscript.user.js',
// // :
// ),
// :
//
//【設定例】
// [レジストリ](https://github.com/furyutei/twMediaDownloader)
// [対象となるユーザースクリプトの場所](https://github.com/furyutei/twMediaDownloader/blob/master/src/js/twMediaDownloader.user.js)
// の場合
//'furyutei/twMediaDownloader' => array(
// 'src/js/twMediaDownloader.user.js',
// ),
);
// }
// === Main processing {
main();
// }
// === Functions {
function main() {
global $USERJS_PATH_MAP;
header( 'Content-Type: text/plain; charset=utf-8' );
$payload = get_webhook_payload();
if ( @$payload[ 'error' ] ) {
set_status( 401 );
return;
}
$ref = @$payload[ 'ref' ] ?: '';
$repository = @$payload[ 'repository' ] ?: array();
$master_branch_name = @$repository[ 'master_branch' ];
write_log_debug( "ref: '{$ref}', master_branch_name: '{$master_branch_name}'" );
if ( $ref ) {
// 'refs/heads/<branch_name>'
list( $dir1, $dir2, $branch_name ) = explode( '/', $ref, 3 ) + array( '', '', '' );
if ( $master_branch_name !== $branch_name ) {
set_status( 200 );
write_log( "Note: '{$branch_name}' is not master branch" );
return;
}
}
$full_name = @$repository[ 'full_name' ];
$html_url = @$repository[ 'html_url' ];
$owner = @$repository[ 'owner' ][ 'login' ];
$userjs_list = @$USERJS_PATH_MAP[ $full_name ];
if ( ( ! $userjs_list ) || ( ! $html_url ) || ( ! $owner ) ) {
set_status( 400 );
return;
}
$output_dir = realpath( $_SERVER[ 'DOCUMENT_ROOT' ] . '/userjs' ) . '/' . $owner;
write_log_debug( "output_dir: {$output_dir}" );
if ( ( ! file_exists( $output_dir ) ) && ( ! mkdir( $output_dir, 0777, TRUE ) ) ) {
set_status( 500 );
write_log( "Error: Failed to create directory: {$output_dir}" );
return;
}
foreach( $userjs_list as $userjs_path ) {
$userjs_url = "{$html_url}/raw/master/{$userjs_path}";
$userjs_filepath = $output_dir . '/' . basename( $userjs_path );
write_log( "Source URL: {$userjs_url}" );
write_log( "Destination file: {$userjs_filepath}" );
file_put_contents( $userjs_filepath, file_get_contents( $userjs_url ) );
}
set_status( 200 );
} // end of main()
function is_debug_mode() {
return ( defined( 'DEBUG_FLAG' ) && DEBUG_FLAG );
} // end of is_debug_mode()
function write_log_debug( $message ) {
if ( ! is_debug_mode() ) {
return;
}
write_log( $message );
} // end of write_log_debug()
function write_log( $message ) {
global $LOG_FILE;
$remote_addr = @$_SERVER[ 'REMOTE_ADDR' ] ?: '(localhost)';
$log_message = date( 'Y-m-d H:i:s' ) . ' [' . $remote_addr . '] ' . $message . "\n";
file_put_contents( $LOG_FILE, $log_message, FILE_APPEND | LOCK_EX );
} // end of write_log()
function set_status( $status_code ) {
http_response_code( $status_code );
if ( ( 200 <= $status_code ) && ( $status_code < 300 ) ) {
echo( 'OK' );
}
else {
echo( 'NG' );
}
} // end of set_status()
function get_webhook_payload() {
$payload_body = file_get_contents( 'php://input' );
if ( ! is_correct_signature( $payload_body ) ) {
write_log( 'Error: Incorrect signature' );
return array(
'error' => TRUE
);
}
$payload_string = @$_POST[ 'payload' ] ?: $payload_body;
$payload = json_decode( $payload_string, TRUE );
if ( ( $payload === NULL ) && ( json_last_error() !== JSON_ERROR_NONE ) ) {
write_log( 'Error: Incorrect payload' );
return array(
'error' => TRUE
);
}
if ( is_debug_mode() ) {
write_log( '--------------------' );
ob_start();
var_export( $payload );
write_log( "[payload]\n" . ob_get_contents() );
ob_end_clean();
write_log( '--------------------' );
}
return @$payload ?: array();
} // end of get_webhook_payload()
function is_correct_signature( $payload_body ) {
if ( ( ! defined( 'SECRET_KEY' ) ) || ( ! SECRET_KEY ) ) {
return TRUE;
}
$signature = @$_SERVER[ 'HTTP_X_HUB_SIGNATURE' ] ?: NULL;
if ( ! $signature ) {
return FALSE;
}
list( $algo, $hash ) = explode( '=', $signature, 2 ) + array( '', '' );
if ( ! in_array( $algo, hash_algos(), TRUE ) ) {
return FALSE;
}
$hash_calc = hash_hmac( $algo, $payload_body, SECRET_KEY );
if ( $hash !== $hash ) {
return FALSE;
}
return TRUE;
} // end of is_correct_signature()
// }
// end of file

SELinux への対策

ログを書き込んだり、ユーザースクリプトを特定のディレクトリ下に保存しようとすると、SELinux の制限に引っかかる場合があるため、その対策。

# ログ用ディレクトリ作成例(/path/to/webhook/log)
# ※ユーザースクリプト保存用ディレクトリについても同様に設定する

[user@localhost webhook]$ cd /path/to/webhook
[user@localhost webhook]$ mkdir -p log

[user@localhost webhook]$ ls -Zd ./log
drwxrwxr-x. user user unconfined_u:object_r:httpd_user_content_t:s0 ./log

[user@localhost webhook]$ sudo semanage fcontext -a -t httpd_sys_rw_content_t "${PWD}/log(/.*)?"
# semanage では、対象をフルパスで指定する必要あり

[user@localhost webhook]$ sudo semanage fcontext -l -C
:
/path/to/webhook/log(/.*)? all files          system_u:object_r:httpd_sys_rw_content_t:s0
[user@localhost webhook]$ ls -Zd ./log
drwxrwxr-x. user user unconfined_u:object_r:httpd_user_content_t:s0 ./log
# この時点ではまだ変化しない

[user@localhost webhook]$ sudo restorecon -vRF ./log
restorecon reset /path/to/webhook/log context unconfined_u:object_r:httpd_sys_content_t:s0->unconfined_u:object_r:httpd_sys_rw_content_t:s0
# -F オプションを付けないと、変化しない場合があるので注意
# ※セキュリティコンテキストのユーザーが異なる場合等("unconfined_u" vs "system_u")
# ※ -v を付けているのに何も表示されなければ、どこか設定がおかしいはず

[user@localhost webhook]$ ls -Zd ./log
drwxrwxr-x. user user system_u:object_r:httpd_sys_rw_content_t:s0 ./log

[user@localhost webhook]$ sudo chown -R apache:apache ./log
# オーナーを変えてしまうと、user が書き換えできなくなるので……
[user@localhost webhook]$ sudo chmod 777 ./log
# 権限を 777 にしておく(user では読むだけの場合は設定不要)
# 配信用ユーザースクリプト(*.user.js)を置くディレクトリ(<DOCUMENT_ROOT>/userjs)に配置する .htaccess の例
<ifModule mod_headers.c>
<filesMatch "\.js$">
Header set Access-Control-Allow-Origin "*"
Header set Content-Type "application/javascript;charset=UTF-8"
</filesMatch>
</ifModule>

ユーザースクリプト(*.user.js)取得時のHTTP Response Header の違い

wget -S --spider で Response Header を取得(結果は多少編集済み)

GitHubからの取得時

[user@localhost webhook]$ wget -S --spider -q https://github.com/furyutei/twOpenOriginalImage/raw/master/src/js/twOpenOriginalImage.user.js -O -

[HTTP/1.1 302 Found]
  Date: Thu, 22 Feb 2018 11:44:43 GMT
  Content-Type: text/html; charset=utf-8
  Server: GitHub.com
  Status: 302 Found
  Cache-Control: no-cache
  Vary: X-PJAX, Accept-Encoding
  X-RateLimit-Limit: 100
  X-RateLimit-Remaining: 100
  Access-Control-Allow-Origin: https://render.githubusercontent.com
  Location: https://raw.githubusercontent.com/furyutei/twOpenOriginalImage/master/src/js/twOpenOriginalImage.user.js
  X-UA-Compatible: IE=Edge,chrome=1
  X-Runtime: 0.060709
  Expect-CT: max-age=2592000, report-uri="https://api.github.com/_private/browser/errors"
  Content-Security-Policy: default-src 'none'; base-uri 'self'; block-all-mixed-content; child-src render.githubusercontent.com; connect-src 'self' uploads.github.com status.github.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com wss://live.github.com; font-src assets-cdn.github.com; form-action 'self' github.com gist.github.com; frame-ancestors 'none'; img-src 'self' data: assets-cdn.github.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src 'self'; media-src 'none'; script-src assets-cdn.github.com; style-src 'unsafe-inline' assets-cdn.github.com; worker-src 'self'
  Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
  X-Content-Type-Options: nosniff
  X-Frame-Options: deny
  X-XSS-Protection: 1; mode=block
  X-Runtime-rack: 0.068345
  Age: 21
  X-GitHub-Request-Id: xxx
  
[HTTP/1.1 200 OK]
  Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox
  Strict-Transport-Security: max-age=31536000
  X-Content-Type-Options: nosniff
  X-Frame-Options: deny
  X-XSS-Protection: 1; mode=block
  ETag: "xxx"
  Content-Type: text/plain; charset=utf-8
  Cache-Control: max-age=300
  X-Geo-Block-List:
  X-GitHub-Request-Id: xxx
  Content-Length: 168266
  Accept-Ranges: bytes
  Date: Thu, 22 Feb 2018 11:45:04 GMT
  Via: 1.1 varnish
  Connection: keep-alive
  X-Served-By: cache-nrt6148-NRT
  X-Cache: HIT
  X-Cache-Hits: 1
  X-Timer: S1519299904.465708,VS0,VE1
  Vary: Authorization,Accept-Encoding
  Access-Control-Allow-Origin: *
  X-Fastly-Request-ID: xxx
  Expires: Thu, 22 Feb 2018 11:50:04 GMT
  Source-Age: 21

自サーバからの取得時

[user@localhost webhook]$  wget -S --spider -q https://furyu.atnifty.com/userjs/furyutei/twOpenOriginalImage.user.js -O -

[HTTP/1.1 200 OK]
  Date: Thu, 22 Feb 2018 11:54:58 GMT
  Server: Apache
  Last-Modified: Thu, 22 Feb 2018 10:17:11 GMT
  ETag: "2914a-565ca58d12671"
  Accept-Ranges: bytes
  Content-Length: 168266
  Access-Control-Allow-Origin: *
  Content-Type: application/javascript
  Keep-Alive: timeout=5, max=100
  Connection: Keep-Alive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment