-
-
Save wsalesky/bf26507ff593f0c99a35 to your computer and use it in GitHub Desktop.
| xquery version "3.0"; | |
| (:module namespace gitsync = "http://syriaca.org/ns/gitsync";:) | |
| (:~ | |
| : XQuery endpoint to respond to Github webhook requests. Query responds only to push requests. | |
| : The EXPath Crypto library supplies the HMAC-SHA1 algorithm for matching Github secret. | |
| : | |
| : Secret can be stored as environmental variable. | |
| : Will need to be run with administrative privileges, suggest creating a git user with privileges only to relevant app. | |
| : | |
| : @author Winona Salesky | |
| : @version 1.1 | |
| : | |
| : @see https://github.com/joewiz/xqjson | |
| : @see http://expath.org/spec/crypto | |
| : @see http://expath.org/spec/http-client | |
| : | |
| :) | |
| import module namespace xdb="http://exist-db.org/xquery/xmldb"; | |
| import module namespace templates="http://exist-db.org/xquery/templates" ; | |
| import module namespace xqjson="http://xqilla.sourceforge.net/lib/xqjson"; | |
| import module namespace crypto="http://expath.org/ns/crypto"; | |
| import module namespace http="http://expath.org/ns/http-client"; | |
| declare option exist:serialize "method=xml media-type=text/xml indent=yes"; | |
| (:~ | |
| : Recursively creates new collections if necessary | |
| : @param $uri url to resource being added to db | |
| :) | |
| declare function local:create-collections($uri as xs:string){ | |
| let $collection-uri := substring($uri,1) | |
| for $collections in tokenize($collection-uri, '/') | |
| let $current-path := concat('/',substring-before($collection-uri, $collections),$collections) | |
| let $parent-collection := substring($current-path, 1, string-length($current-path) - string-length(tokenize($current-path, '/')[last()])) | |
| return | |
| if (xmldb:collection-available($current-path)) then () | |
| else xmldb:create-collection($parent-collection, $collections) | |
| }; | |
| (:~ | |
| : Updates files in eXistdb with github data | |
| : @param $commits serilized json data | |
| : @param $contents-url string pointing to resource on github | |
| :) | |
| declare function local:do-update($commits as node()*, $contents-url as xs:string?){ | |
| for $modified in $commits/descendant::*/*:pair[@name="modified"]/*:item/text() | |
| let $file-path := concat($contents-url, $modified) | |
| let $req := <http:request href="{xs:anyURI($file-path)}" method="get"/> | |
| let $file := http:send-request($req)[2] | |
| let $file-info := | |
| let $payload := util:base64-decode($file) | |
| let $parse-payload := xqjson:parse-json($payload) | |
| return $parse-payload | |
| let $file-data := $file-info//*:pair[@name="content"] | |
| let $collection := xs:anyURI('/db/apps/srophe') | |
| let $file-name := $file-info//*:pair[@name="name"]/text() | |
| let $resource-path := substring-before(replace($modified,'srophe-app/',''),$file-name) | |
| let $collection-uri := concat($collection,'/',$resource-path) | |
| return | |
| try { | |
| if(xmldb:collection-available($collection-uri)) then | |
| <response status="okay"> | |
| <message>{xmldb:store($collection-uri, xmldb:encode-uri($file-name), xs:base64Binary($file-data))}</message> | |
| </response> | |
| else (local:create-collections($collection-uri),xmldb:store($collection-uri, xmldb:encode-uri($file-name), xs:base64Binary($file-data))) | |
| } catch * { | |
| <response status="fail"> | |
| <message>Failed to update resource: {concat($err:code, ": ", $err:description)}</message> | |
| </response> | |
| } | |
| }; | |
| (:~ | |
| : Adds new files to eXistdb. Changes permissions for group write. | |
| : Pulls data from github repository, parses file information and passes data to xmldb:store | |
| : @param $commits serilized json data | |
| : @param $contents-url string pointing to resource on github | |
| : NOTE permission changes could happen in a db trigger after files are created | |
| :) | |
| declare function local:do-add($commits as node()*, $contents-url as xs:string?){ | |
| for $modified in $commits/descendant::*/*:pair[@name="added"]/*:item/text() | |
| let $file-path := concat($contents-url, $modified) | |
| let $req := <http:request href="{xs:anyURI($file-path)}" method="get"/> | |
| let $file := http:send-request($req)[2] | |
| let $file-info := | |
| let $payload := util:base64-decode($file) | |
| let $parse-payload := xqjson:parse-json($payload) | |
| return $parse-payload | |
| let $file-data := $file-info//*:pair[@name="content"] | |
| let $collection := xs:anyURI('/db/apps/srophe') | |
| let $file-name := $file-info//*:pair[@name="name"]/text() | |
| let $resource-path := substring-before(replace($modified,'srophe-app/',''),$file-name) | |
| let $collection-uri := concat($collection,'/',$resource-path) | |
| return | |
| try { | |
| if(xmldb:collection-available($collection-uri)) then | |
| <response status="okay"> | |
| <message> | |
| { | |
| ( | |
| xmldb:store($collection-uri, xmldb:encode-uri($file-name), xs:base64Binary($file-data)), | |
| sm:chmod(xs:anyURI(concat($collection-uri,$file-name)), 'rwxrwxr-x'), | |
| sm:chgrp(xs:anyURI(concat($collection-uri,$file-name)), 'srophe') | |
| ) | |
| } | |
| </message> | |
| </response> | |
| else | |
| <response status="okay"> | |
| <message> | |
| { | |
| ( | |
| local:create-collections($collection-uri), | |
| xmldb:store($collection-uri, xmldb:encode-uri($file-name), xs:base64Binary($file-data)), | |
| sm:chmod(xs:anyURI(concat($collection-uri,$file-name)), 'rwxrwxr-x'), | |
| sm:chgrp(xs:anyURI(concat($collection-uri,$file-name)), 'srophe') | |
| )} | |
| </message> | |
| </response> | |
| } catch * { | |
| <response status="fail"> | |
| <message>Failed to add resource: {concat($err:code, ": ", $err:description)}</message> | |
| </response> | |
| } | |
| }; | |
| (:~ | |
| : Removes files from the database uses xmldb:remove | |
| : Pulls data from github repository, parses file information and passes data to xmldb:store | |
| : @param $commits serilized json data | |
| : @param $contents-url string pointing to resource on github | |
| :) | |
| declare function local:do-delete($commits as node()*, $contents-url as xs:string?){ | |
| for $modified in $commits/descendant::*/*:pair[@name="removed"]/*:item/text() | |
| let $file-path := concat($contents-url, $modified) | |
| let $collection := xs:anyURI('/db/apps/srophe') | |
| let $file-name := tokenize($modified,'/')[last()] | |
| let $resource-path := substring-before(replace($modified,'srophe-app/',''),$file-name) | |
| let $collection-uri := replace(concat($collection,'/',$resource-path),'/$','') | |
| return | |
| try { | |
| <response status="okay"> | |
| <message>{xmldb:remove($collection-uri, $file-name)}</message> | |
| </response> | |
| } catch * { | |
| <response status="fail"> | |
| <message>Failed to remove resource: {concat($err:code, ": ", $err:description)}</message> | |
| </response> | |
| } | |
| }; | |
| (:~ | |
| : Parse request data and pass to appropriate local functions | |
| : @param $json-data github response serializing as xml xqjson:parse-json() | |
| :) | |
| declare function local:parse-request($json-data){ | |
| let $contents-url := substring-before($json-data//*:pair[@name="contents_url"]/text(),'{') | |
| return | |
| try { | |
| if($json-data//*:pair[@name="ref"] = "refs/heads/master") then | |
| if($json-data//*:pair[@name="commits"]) then | |
| let $commits := $json-data//*:pair[@name="commits"] | |
| return | |
| (if($commits/descendant::*/*:pair[@name="modified"]/*:item/text()) then | |
| local:do-update($commits, $contents-url) | |
| else (), | |
| if($commits/descendant::*/*:pair[@name="added"]/*:item/text()) then | |
| local:do-add($commits, $contents-url) | |
| else (), | |
| if($commits/descendant::*/*:pair[@name="removed"]/*:item/text()) then | |
| local:do-delete($commits, $contents-url) | |
| else ()) | |
| else <response status="fail"><message>This is a GitHub request, however there were no commits.</message></response> | |
| else <response status="fail"><message>Not from the master branch.</message></response> | |
| } catch * { | |
| <response status="fail"> | |
| <message>{concat($err:code, ": ", $err:description)}</message> | |
| </response> | |
| } | |
| }; | |
| (:~ | |
| : Validate github post request. | |
| : Check user agent and github event, only accept push events from master branch. | |
| : Check git hook secret against secret stored in environmental variable | |
| : @param $GIT_TOKEN environment variable storing github secret | |
| :) | |
| let $post-data := request:get-data() | |
| return | |
| if(not(empty($post-data))) then | |
| let $payload := util:base64-decode(request:get-data()) | |
| let $json-data := xqjson:parse-json($payload) | |
| return | |
| try { | |
| if(matches(request:get-header('User-Agent'), '^GitHub-Hookshot/')) then | |
| if(request:get-header('X-GitHub-Event') = 'push') then | |
| let $signiture := request:get-header('X-Hub-Signature') | |
| let $expected-result := <expected-result>{request:get-header('X-Hub-Signature')}</expected-result> | |
| let $private-key := string(environment-variable('GIT_TOKEN')) | |
| let $actual-result := | |
| <actual-result> | |
| {concat('sha1=',crypto:hmac($payload, $private-key, "HMAC-SHA-1", "hex"))} | |
| </actual-result> | |
| let $condition := normalize-space($expected-result/text()) = normalize-space($actual-result/text()) | |
| return | |
| if ($condition) then | |
| local:parse-request($json-data) | |
| else | |
| <response status="fail"><message>Invalid secret.</message></response> | |
| else <response status="fail"><message>Invalid trigger.</message></response> | |
| else <response status="fail"><message>This is not a GitHub request.</message></response> | |
| } catch * { | |
| <response status="fail"> | |
| <message>Unacceptable headers {concat($err:code, ": ", $err:description)}</message> | |
| </response> | |
| } | |
| else | |
| <response status="fail"> | |
| <message>No post data recieved</message> | |
| </response> |
Nice works......
Thanks.
Updated to handle empty requests.
Set script to use setuid to execute with elevated privileges:
example:
(
sm:chown(xs:anyURI('/db/apps/srophe/git-sync.xql'), "admin"),
sm:chgrp(xs:anyURI('/db/apps/srophe/git-sync.xql'), "dba"),
sm:chmod(xs:anyURI('/db/apps/srophe/git-sync.xql'), "rwsr-xr-x")
)
Watch out for the bulk upload gotcha. Github only allows 60 request per hour with this type of request. For a more robust application integration of OAuth looks necessary.
To solve the request limits for unauthorized requests, you will need to generate either a github personal access token or register your application as an OAuth application via you settings. I used the personal access token. You can then add the following:
let $gitToken:= 'YOUR TOKEN'
let $send :=
<http:request href="https://api.github.com" method="GET">
<http:header name="Authorization" value="{concat('token ',$gitToken)}"/>
</http:request>
return http:send-request($send)
This will increase you rate limit to 5,000.
Added create collection function