From e63051cdef2552f9ba55f5adf1aeb4e095d08491 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Thu, 15 Nov 2018 12:44:47 -0600 Subject: [PATCH 01/17] Update to the latest template app --- .env-example | 3 ++ .gitignore | 3 ++ advanced_server.rb | 3 +- server.rb | 71 ++++++++++++++++++---------------------------- 4 files changed, 35 insertions(+), 45 deletions(-) create mode 100644 .env-example create mode 100644 .gitignore diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..fbf6aeb --- /dev/null +++ b/.env-example @@ -0,0 +1,3 @@ +GITHUB_PRIVATE_KEY= +GITHUB_APP_IDENTIFIER= +GITHUB_WEBHOOK_SECRET= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7545483 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +.env diff --git a/advanced_server.rb b/advanced_server.rb index d617d5a..4d4597a 100644 --- a/advanced_server.rb +++ b/advanced_server.rb @@ -73,8 +73,7 @@ class GHAapp < Sinatra::Application end end - 'ok' # we have to return _something_ ;) - + 'ok' # You have to return _something_. ;) end diff --git a/server.rb b/server.rb index 9fd6b7b..9f129ab 100644 --- a/server.rb +++ b/server.rb @@ -1,26 +1,28 @@ require 'sinatra' require 'octokit' +require 'dotenv/load' # Manages environment variables +require 'git' require 'json' -require 'openssl' # Used to verify the webhook signature -require 'jwt' # Used to authenticate a GitHub App -require 'time' # Used to get ISO 8601 representation of a Time object -require 'logger' +require 'openssl' # Verifies the webhook signature +require 'jwt' # Authenticates a GitHub App +require 'time' # Gets ISO 8601 representation of a Time object +require 'logger' # Logs debug statements set :port, 3000 +set :bind, '0.0.0.0' # This is template code to create a GitHub App server. # You can read more about GitHub Apps here: # https://developer.github.com/apps/ # # On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add fun functionality! +# It's up to you to add functionality! # You can check out one example in advanced_server.rb. # # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. # -# # Of course, not all apps need to receive and process events! # Feel free to rip out the event handling code if you don't need it. # @@ -29,16 +31,7 @@ class GHAapp < Sinatra::Application - # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! - # Instead, set and read app tokens or other secrets in your code - # in a runtime source, like an environment variable like below - - # Expects that the private key has been set as an environment variable in - # PEM format using the following command to replace newlines with the - # literal `\n`: - # export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem` - # - # Converts the newlines + # Expects that the private key in PEM format. Converts the newlines PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a secret set. The secret is used to verify @@ -59,26 +52,25 @@ class GHAapp < Sinatra::Application get_payload_request(request) verify_webhook_signature authenticate_app - # Authenticate each installation of the app in order to run API operations + # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do - # # # # # # # # # # # # # # # # # # # - # ADD YOUR CODE HERE # - # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # + # ADD YOUR CODE HERE # + # # # # # # # # # # # # - 'ok' # We've got to return _something_. ;) end helpers do - # # # # # # # # # # # # # # # # # # # - # ADD YOUR HELPERS METHODS HERE # - # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # + # ADD YOUR HELPER METHODS HERE # + # # # # # # # # # # # # # # # # # # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) @@ -95,7 +87,7 @@ def get_payload_request(request) end # Instantiate an Octokit client authenticated as a GitHub App. - # GitHub App authentication equires that we construct a + # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app an not altererd by # a malicious third party. @@ -111,15 +103,15 @@ def authenticate_app iss: APP_IDENTIFIER } - # Cryptographically sign the JWT + # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end - # Instantiate an Octokit client authenticated as an installation of a - # GitHub App to run API operations. + # Instantiate an Octokit client, authenticated as an installation of a + # GitHub App, to run API operations. def authenticate_installation(payload) installation_id = payload['installation']['id'] installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] @@ -129,14 +121,14 @@ def authenticate_installation(payload) # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # - # GitHub will the WEBHOOK_SECRET, registered - # to the GitHub App, to create a hash signature sent in each webhook payload - # in the `X-HUB-Signature` header. This code computes the expected hash - # signature and compares it to the signature sent in the `X-HUB-Signature` - # header. If they don't match, this request is an attack, and we should - # reject it. GitHub uses the HMAC hexdigest to compute the signature. The - # `X-HUB-Signature` looks something like this: "sha1=123456" - # See https://developer.github.com/webhooks/securing/ for details + # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to + # create the hash signature sent in the `X-HUB-Signature` header of each + # webhook. This code computes the expected hash signature and compares it to + # the signature sent in the `X-HUB-Signature` header. If they don't match, + # this request is an attack, and you should reject it. GitHub uses the HMAC + # hexdigest to compute the signature. The `X-HUB-Signature` looks something + # like this: "sha1=123456". + # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -150,12 +142,5 @@ def verify_webhook_signature end end - - - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code ;) But, for the curious: - # $0 is the executed file - # __FILE__ is the current file - # If they are the same—that is, we are running this file directly, call the Sinatra run method run! if __FILE__ == $0 end From c6cdf6c84b10f75e2073e1e779bf62011c069715 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Thu, 15 Nov 2018 12:49:02 -0600 Subject: [PATCH 02/17] Align with template code --- server.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server.rb b/server.rb index 9f129ab..712c1f5 100644 --- a/server.rb +++ b/server.rb @@ -1,7 +1,6 @@ require 'sinatra' require 'octokit' require 'dotenv/load' # Manages environment variables -require 'git' require 'json' require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App @@ -63,6 +62,7 @@ class GHAapp < Sinatra::Application # ADD YOUR CODE HERE # # # # # # # # # # # # # + 200 # success status end @@ -142,5 +142,11 @@ def verify_webhook_signature end end + + # Finally some logic to let us run this server directly from the commandline, or with Rack + # Don't worry too much about this code. But, for the curious: + # $0 is the executed file + # __FILE__ is the current file + # If they are the same—that is, we are running this file directly, call the Sinatra run method run! if __FILE__ == $0 end From 20716ee653f671ebcea1659d252b6c84fdc437f7 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Thu, 15 Nov 2018 12:50:29 -0600 Subject: [PATCH 03/17] normalize file names --- server.rb | 73 ++++++++++++++---------- advanced_server.rb => template_server.rb | 73 ++++++++++-------------- 2 files changed, 73 insertions(+), 73 deletions(-) rename advanced_server.rb => template_server.rb (66%) diff --git a/server.rb b/server.rb index 712c1f5..4d4597a 100644 --- a/server.rb +++ b/server.rb @@ -1,27 +1,26 @@ require 'sinatra' require 'octokit' -require 'dotenv/load' # Manages environment variables require 'json' -require 'openssl' # Verifies the webhook signature -require 'jwt' # Authenticates a GitHub App -require 'time' # Gets ISO 8601 representation of a Time object -require 'logger' # Logs debug statements +require 'openssl' # Used to verify the webhook signature +require 'jwt' # Used to authenticate a GitHub App +require 'time' # Used to get ISO 8601 representation of a Time object +require 'logger' set :port, 3000 -set :bind, '0.0.0.0' # This is template code to create a GitHub App server. # You can read more about GitHub Apps here: # https://developer.github.com/apps/ # # On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add functionality! +# It's up to you to add fun functionality! # You can check out one example in advanced_server.rb. # # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. # +# # Of course, not all apps need to receive and process events! # Feel free to rip out the event handling code if you don't need it. # @@ -30,7 +29,16 @@ class GHAapp < Sinatra::Application - # Expects that the private key in PEM format. Converts the newlines + # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! + # Instead, set and read app tokens or other secrets in your code + # in a runtime source, like an environment variable like below + + # Expects that the private key has been set as an environment variable in + # PEM format using the following command to replace newlines with the + # literal `\n`: + # export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem` + # + # Converts the newlines PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a secret set. The secret is used to verify @@ -51,26 +59,32 @@ class GHAapp < Sinatra::Application get_payload_request(request) verify_webhook_signature authenticate_app - # Authenticate the app installation in order to run API operations + # Authenticate each installation of the app in order to run API operations authenticate_installation(@payload) end post '/event_handler' do - # # # # # # # # # # # # - # ADD YOUR CODE HERE # - # # # # # # # # # # # # + case request.env['HTTP_X_GITHUB_EVENT'] + when 'issues' + if @payload['action'] === 'opened' + handle_issue_opened_event(@payload) + end + end - 200 # success status + 'ok' # You have to return _something_. ;) end helpers do - # # # # # # # # # # # # # # # # # - # ADD YOUR HELPER METHODS HERE # - # # # # # # # # # # # # # # # # # + # When an issue is opened, add a label + def handle_issue_opened_event(payload) + repo = payload['repository']['full_name'] + issue_number = payload['issue']['number'] + @installation_client.add_labels_to_an_issue(repo, issue_number, ['needs-response']) + end # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) @@ -87,7 +101,7 @@ def get_payload_request(request) end # Instantiate an Octokit client authenticated as a GitHub App. - # GitHub App authentication requires that you construct a + # GitHub App authentication equires that we construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app an not altererd by # a malicious third party. @@ -103,15 +117,15 @@ def authenticate_app iss: APP_IDENTIFIER } - # Cryptographically sign the JWT. + # Cryptographically sign the JWT jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end - # Instantiate an Octokit client, authenticated as an installation of a - # GitHub App, to run API operations. + # Instantiate an Octokit client authenticated as an installation of a + # GitHub App to run API operations. def authenticate_installation(payload) installation_id = payload['installation']['id'] installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] @@ -121,14 +135,14 @@ def authenticate_installation(payload) # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # - # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to - # create the hash signature sent in the `X-HUB-Signature` header of each - # webhook. This code computes the expected hash signature and compares it to - # the signature sent in the `X-HUB-Signature` header. If they don't match, - # this request is an attack, and you should reject it. GitHub uses the HMAC - # hexdigest to compute the signature. The `X-HUB-Signature` looks something - # like this: "sha1=123456". - # See https://developer.github.com/webhooks/securing/ for details. + # GitHub will the WEBHOOK_SECRET, registered + # to the GitHub App, to create a hash signature sent in each webhook payload + # in the `X-HUB-Signature` header. This code computes the expected hash + # signature and compares it to the signature sent in the `X-HUB-Signature` + # header. If they don't match, this request is an attack, and we should + # reject it. GitHub uses the HMAC hexdigest to compute the signature. The + # `X-HUB-Signature` looks something like this: "sha1=123456" + # See https://developer.github.com/webhooks/securing/ for details def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -143,8 +157,9 @@ def verify_webhook_signature end + # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code. But, for the curious: + # Don't worry too much about this code ;) But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the Sinatra run method diff --git a/advanced_server.rb b/template_server.rb similarity index 66% rename from advanced_server.rb rename to template_server.rb index 4d4597a..712c1f5 100644 --- a/advanced_server.rb +++ b/template_server.rb @@ -1,26 +1,27 @@ require 'sinatra' require 'octokit' +require 'dotenv/load' # Manages environment variables require 'json' -require 'openssl' # Used to verify the webhook signature -require 'jwt' # Used to authenticate a GitHub App -require 'time' # Used to get ISO 8601 representation of a Time object -require 'logger' +require 'openssl' # Verifies the webhook signature +require 'jwt' # Authenticates a GitHub App +require 'time' # Gets ISO 8601 representation of a Time object +require 'logger' # Logs debug statements set :port, 3000 +set :bind, '0.0.0.0' # This is template code to create a GitHub App server. # You can read more about GitHub Apps here: # https://developer.github.com/apps/ # # On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add fun functionality! +# It's up to you to add functionality! # You can check out one example in advanced_server.rb. # # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. # -# # Of course, not all apps need to receive and process events! # Feel free to rip out the event handling code if you don't need it. # @@ -29,16 +30,7 @@ class GHAapp < Sinatra::Application - # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! - # Instead, set and read app tokens or other secrets in your code - # in a runtime source, like an environment variable like below - - # Expects that the private key has been set as an environment variable in - # PEM format using the following command to replace newlines with the - # literal `\n`: - # export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem` - # - # Converts the newlines + # Expects that the private key in PEM format. Converts the newlines PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a secret set. The secret is used to verify @@ -59,32 +51,26 @@ class GHAapp < Sinatra::Application get_payload_request(request) verify_webhook_signature authenticate_app - # Authenticate each installation of the app in order to run API operations + # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do - case request.env['HTTP_X_GITHUB_EVENT'] - when 'issues' - if @payload['action'] === 'opened' - handle_issue_opened_event(@payload) - end - end + # # # # # # # # # # # # + # ADD YOUR CODE HERE # + # # # # # # # # # # # # - 'ok' # You have to return _something_. ;) + 200 # success status end helpers do - # When an issue is opened, add a label - def handle_issue_opened_event(payload) - repo = payload['repository']['full_name'] - issue_number = payload['issue']['number'] - @installation_client.add_labels_to_an_issue(repo, issue_number, ['needs-response']) - end + # # # # # # # # # # # # # # # # # + # ADD YOUR HELPER METHODS HERE # + # # # # # # # # # # # # # # # # # # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) @@ -101,7 +87,7 @@ def get_payload_request(request) end # Instantiate an Octokit client authenticated as a GitHub App. - # GitHub App authentication equires that we construct a + # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app an not altererd by # a malicious third party. @@ -117,15 +103,15 @@ def authenticate_app iss: APP_IDENTIFIER } - # Cryptographically sign the JWT + # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end - # Instantiate an Octokit client authenticated as an installation of a - # GitHub App to run API operations. + # Instantiate an Octokit client, authenticated as an installation of a + # GitHub App, to run API operations. def authenticate_installation(payload) installation_id = payload['installation']['id'] installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] @@ -135,14 +121,14 @@ def authenticate_installation(payload) # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # - # GitHub will the WEBHOOK_SECRET, registered - # to the GitHub App, to create a hash signature sent in each webhook payload - # in the `X-HUB-Signature` header. This code computes the expected hash - # signature and compares it to the signature sent in the `X-HUB-Signature` - # header. If they don't match, this request is an attack, and we should - # reject it. GitHub uses the HMAC hexdigest to compute the signature. The - # `X-HUB-Signature` looks something like this: "sha1=123456" - # See https://developer.github.com/webhooks/securing/ for details + # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to + # create the hash signature sent in the `X-HUB-Signature` header of each + # webhook. This code computes the expected hash signature and compares it to + # the signature sent in the `X-HUB-Signature` header. If they don't match, + # this request is an attack, and you should reject it. GitHub uses the HMAC + # hexdigest to compute the signature. The `X-HUB-Signature` looks something + # like this: "sha1=123456". + # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -157,9 +143,8 @@ def verify_webhook_signature end - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code ;) But, for the curious: + # Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the Sinatra run method From 177a288244d41d8a68afa31f77a91519aec2c93a Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Thu, 15 Nov 2018 13:14:05 -0600 Subject: [PATCH 04/17] Remove package-lock.json from gitignore --- .gitignore | 1 - server.rb | 55 +++++++++++++++++++++++------------------------------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 7545483..37d7e73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules -package-lock.json .env diff --git a/server.rb b/server.rb index 4d4597a..0890dc6 100644 --- a/server.rb +++ b/server.rb @@ -1,26 +1,27 @@ require 'sinatra' require 'octokit' +require 'dotenv/load' # Manages environment variables require 'json' -require 'openssl' # Used to verify the webhook signature -require 'jwt' # Used to authenticate a GitHub App -require 'time' # Used to get ISO 8601 representation of a Time object -require 'logger' +require 'openssl' # Verifies the webhook signature +require 'jwt' # Authenticates a GitHub App +require 'time' # Gets ISO 8601 representation of a Time object +require 'logger' # Logs debug statements set :port, 3000 +set :bind, '0.0.0.0' # This is template code to create a GitHub App server. # You can read more about GitHub Apps here: # https://developer.github.com/apps/ # # On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add fun functionality! +# It's up to you to add functionality! # You can check out one example in advanced_server.rb. # # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. # -# # Of course, not all apps need to receive and process events! # Feel free to rip out the event handling code if you don't need it. # @@ -29,16 +30,7 @@ class GHAapp < Sinatra::Application - # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! - # Instead, set and read app tokens or other secrets in your code - # in a runtime source, like an environment variable like below - - # Expects that the private key has been set as an environment variable in - # PEM format using the following command to replace newlines with the - # literal `\n`: - # export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem` - # - # Converts the newlines + # Expects that the private key in PEM format. Converts the newlines PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a secret set. The secret is used to verify @@ -59,7 +51,7 @@ class GHAapp < Sinatra::Application get_payload_request(request) verify_webhook_signature authenticate_app - # Authenticate each installation of the app in order to run API operations + # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end @@ -73,7 +65,7 @@ class GHAapp < Sinatra::Application end end - 'ok' # You have to return _something_. ;) + 200 # success status end @@ -101,7 +93,7 @@ def get_payload_request(request) end # Instantiate an Octokit client authenticated as a GitHub App. - # GitHub App authentication equires that we construct a + # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app an not altererd by # a malicious third party. @@ -117,15 +109,15 @@ def authenticate_app iss: APP_IDENTIFIER } - # Cryptographically sign the JWT + # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end - # Instantiate an Octokit client authenticated as an installation of a - # GitHub App to run API operations. + # Instantiate an Octokit client, authenticated as an installation of a + # GitHub App, to run API operations. def authenticate_installation(payload) installation_id = payload['installation']['id'] installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] @@ -135,14 +127,14 @@ def authenticate_installation(payload) # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # - # GitHub will the WEBHOOK_SECRET, registered - # to the GitHub App, to create a hash signature sent in each webhook payload - # in the `X-HUB-Signature` header. This code computes the expected hash - # signature and compares it to the signature sent in the `X-HUB-Signature` - # header. If they don't match, this request is an attack, and we should - # reject it. GitHub uses the HMAC hexdigest to compute the signature. The - # `X-HUB-Signature` looks something like this: "sha1=123456" - # See https://developer.github.com/webhooks/securing/ for details + # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to + # create the hash signature sent in the `X-HUB-Signature` header of each + # webhook. This code computes the expected hash signature and compares it to + # the signature sent in the `X-HUB-Signature` header. If they don't match, + # this request is an attack, and you should reject it. GitHub uses the HMAC + # hexdigest to compute the signature. The `X-HUB-Signature` looks something + # like this: "sha1=123456". + # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -157,9 +149,8 @@ def verify_webhook_signature end - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code ;) But, for the curious: + # Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the Sinatra run method From c7ed6ea89607fbb46959801787aaa48a51d46b06 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Tue, 20 Nov 2018 12:30:11 -0600 Subject: [PATCH 05/17] Require dotenv --- Gemfile | 1 + Gemfile.lock | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 0799a52..6b23a59 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,4 @@ source 'http://rubygems.org' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' +gem 'dotenv' diff --git a/Gemfile.lock b/Gemfile.lock index 5cbe9b9..6c57b34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,24 +3,25 @@ GEM specs: addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) - faraday (0.15.2) + dotenv (2.5.0) + faraday (0.15.3) multipart-post (>= 1.2, < 3) jwt (2.1.0) multipart-post (2.0.0) - mustermann (1.0.2) - octokit (4.9.0) + mustermann (1.0.3) + octokit (4.13.0) sawyer (~> 0.8.0, >= 0.5.3) - public_suffix (3.0.2) - rack (2.0.5) - rack-protection (2.0.3) + public_suffix (3.0.3) + rack (2.0.6) + rack-protection (2.0.4) rack sawyer (0.8.1) addressable (>= 2.3.5, < 2.6) faraday (~> 0.8, < 1.0) - sinatra (2.0.3) + sinatra (2.0.4) mustermann (~> 1.0) rack (~> 2.0) - rack-protection (= 2.0.3) + rack-protection (= 2.0.4) tilt (~> 2.0) tilt (2.0.8) @@ -28,9 +29,10 @@ PLATFORMS ruby DEPENDENCIES + dotenv jwt (~> 2.1) octokit (~> 4.0) sinatra (~> 2.0) BUNDLED WITH - 1.14.6 + 1.17.1 From e642c60d6c942a717f9e8879b7dcf65792cc372d Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Tue, 20 Nov 2018 15:50:48 -0600 Subject: [PATCH 06/17] Add globals --- server.rb | 6 +++--- template_server.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server.rb b/server.rb index 0890dc6..2fcb069 100644 --- a/server.rb +++ b/server.rb @@ -119,9 +119,9 @@ def authenticate_app # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) - installation_id = payload['installation']['id'] - installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] - @installation_client = Octokit::Client.new(bearer_token: installation_token) + @installation_id = payload['installation']['id'] + @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] + @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by diff --git a/template_server.rb b/template_server.rb index 712c1f5..5972df0 100644 --- a/template_server.rb +++ b/template_server.rb @@ -113,9 +113,9 @@ def authenticate_app # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) - installation_id = payload['installation']['id'] - installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] - @installation_client = Octokit::Client.new(bearer_token: installation_token) + @installation_id = payload['installation']['id'] + @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] + @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by From dc51a68d21d06011b504cfd49541f29fdb0558f2 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Tue, 27 Nov 2018 16:53:15 -0600 Subject: [PATCH 07/17] Use same base code as template-github-app --- server.rb | 34 +++++++++------------------------- template_server.rb | 9 +++++---- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/server.rb b/server.rb index 2fcb069..7733687 100644 --- a/server.rb +++ b/server.rb @@ -10,30 +10,13 @@ set :port, 3000 set :bind, '0.0.0.0' - -# This is template code to create a GitHub App server. -# You can read more about GitHub Apps here: # https://developer.github.com/apps/ -# -# On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add functionality! -# You can check out one example in advanced_server.rb. -# -# This code is a Sinatra app, for two reasons: -# 1. Because the app will require a landing page for installation. -# 2. To easily handle webhook events. -# -# Of course, not all apps need to receive and process events! -# Feel free to rip out the event handling code if you don't need it. -# -# Have fun! -# - class GHAapp < Sinatra::Application - # Expects that the private key in PEM format. Converts the newlines + # Converts the newlines. Expects that the private key has been set as an + # environment variable in PEM format. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) - # Your registered app must have a secret set. The secret is used to verify + # Your registered app must set have a secret set. The secret is used to verify # that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] @@ -46,10 +29,10 @@ class GHAapp < Sinatra::Application end - # Before each request to the `/event_handler` route + # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) - verify_webhook_signature + verify_webhook_signature! authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) @@ -149,10 +132,11 @@ def verify_webhook_signature end - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code. But, for the curious: + # Finally some logic to let us run this server directly from the command line, + # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file - # If they are the same—that is, we are running this file directly, call the Sinatra run method + # If they are the same—that is, we are running this file directly, call the + # Sinatra run method run! if __FILE__ == $0 end diff --git a/template_server.rb b/template_server.rb index 5972df0..deb8e20 100644 --- a/template_server.rb +++ b/template_server.rb @@ -49,7 +49,7 @@ class GHAapp < Sinatra::Application # Before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) - verify_webhook_signature + verify_webhook_signature! authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) @@ -143,10 +143,11 @@ def verify_webhook_signature end - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code. But, for the curious: + # Finally some logic to let us run this server directly from the command line, + # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file - # If they are the same—that is, we are running this file directly, call the Sinatra run method + # If they are the same—that is, we are running this file directly, call the + # Sinatra run method run! if __FILE__ == $0 end From ef1fe1678c8b423ddc24940e6f83c840ee428381 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Tue, 27 Nov 2018 17:02:39 -0600 Subject: [PATCH 08/17] Remove exclamation from method signature --- server.rb | 4 ++-- template_server.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server.rb b/server.rb index 7733687..27b976a 100644 --- a/server.rb +++ b/server.rb @@ -16,7 +16,7 @@ class GHAapp < Sinatra::Application # environment variable in PEM format. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) - # Your registered app must set have a secret set. The secret is used to verify + # Your registered app must have a secret set. The secret is used to verify # that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] @@ -32,7 +32,7 @@ class GHAapp < Sinatra::Application # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) - verify_webhook_signature! + verify_webhook_signature authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) diff --git a/template_server.rb b/template_server.rb index deb8e20..518be21 100644 --- a/template_server.rb +++ b/template_server.rb @@ -49,7 +49,7 @@ class GHAapp < Sinatra::Application # Before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) - verify_webhook_signature! + verify_webhook_signature authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) From 4c1ed484708ae521fded9910f5072f0bd0e8880e Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Tue, 27 Nov 2018 20:35:15 -0600 Subject: [PATCH 09/17] Add quotes --- .env-example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env-example b/.env-example index fbf6aeb..0ea477b 100644 --- a/.env-example +++ b/.env-example @@ -1,3 +1,3 @@ -GITHUB_PRIVATE_KEY= +GITHUB_PRIVATE_KEY="" GITHUB_APP_IDENTIFIER= GITHUB_WEBHOOK_SECRET= From dec7f119aa65d752c9e49e3952da8f14349e5fd0 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Wed, 28 Nov 2018 17:47:19 -0600 Subject: [PATCH 10/17] Update with new Quickstart information --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2a8f4f9..ddc9341 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ -This is the sample project built by following the "[Building Your First GitHub App](https://developer.github.com/apps/building-your-first-github-app)" Quickstart guide on developer.github.com. +This is an example GitHub App that adds a label to all new issues opened in a repository. You can follow the "[Building your first GitHub App](https://developer.github.com/apps/quickstart-guide/building-your-first-github-app)" Quickstart guide on developer.github.com to learn how to build the app code in `server.rb`. -It consists of two different servers: `server.rb` (boilerplate) and `advanced_server.rb` (completed project). +This project listens for webhook events and uses the Octokit.rb library to make REST API calls. This example project consists of two different servers: +* `template_server.rb` (GitHub App template code) +* `server.rb` (completed project) -## Install and run +To learn how to set up a template GitHub App, follow the "[Configuring a GitHub App](https://developer.github.com/apps/quickstart-guide/configuring-ast-github-app)" Quickstart guide on developer.github.com. + +## Install To run the code, make sure you have [Bundler](http://gembundler.com/) installed; then enter `bundle install` on the command line. -* For the boilerplate project, enter `ruby server.rb` on the command line. +## Set environment variables + +1. Create a copy of the `.env-example` file called `.env`. +2. Add your GitHub App's private key, app ID, and webhook secret to the `.env` file. -* For the completed project, enter `ruby advanced_server.rb` on the command line. +## Run the server -Both commands will run the server at `localhost:3000`. +1. Run `ruby template_server.rb` or `ruby server.rb` on the command line. +1. View the default Sinatra app at `localhost:3000`. From bd735229933e87182d366cfdfe419251fbec273d Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Mon, 10 Dec 2018 10:37:12 -0600 Subject: [PATCH 11/17] Correct spelling --- template_server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template_server.rb b/template_server.rb index 518be21..5a945d0 100644 --- a/template_server.rb +++ b/template_server.rb @@ -137,7 +137,7 @@ def verify_webhook_signature # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. - logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}" + logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end From 4ac138c318d014c7cfc661d1d506e772cb509699 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Mon, 10 Dec 2018 10:37:32 -0600 Subject: [PATCH 12/17] Correct spelling --- server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.rb b/server.rb index 27b976a..f75b66d 100644 --- a/server.rb +++ b/server.rb @@ -126,7 +126,7 @@ def verify_webhook_signature # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. - logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}" + logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end From b076931c65778817685eb9e200721451e3937803 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Mon, 17 Dec 2018 15:18:02 -0600 Subject: [PATCH 13/17] Update to latest guide name --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddc9341..7da0ac7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -This is an example GitHub App that adds a label to all new issues opened in a repository. You can follow the "[Building your first GitHub App](https://developer.github.com/apps/quickstart-guide/building-your-first-github-app)" Quickstart guide on developer.github.com to learn how to build the app code in `server.rb`. +This is an example GitHub App that adds a label to all new issues opened in a repository. You can follow the "[Using the GitHub API in your app](https://developer.github.com/apps/quickstart-guide/using-the-github-api-in-your-app)" quickstart guide on developer.github.com to learn how to build the app code in `server.rb`. This project listens for webhook events and uses the Octokit.rb library to make REST API calls. This example project consists of two different servers: * `template_server.rb` (GitHub App template code) * `server.rb` (completed project) -To learn how to set up a template GitHub App, follow the "[Configuring a GitHub App](https://developer.github.com/apps/quickstart-guide/configuring-ast-github-app)" Quickstart guide on developer.github.com. +To learn how to set up a template GitHub App, follow the "[Setting up your development environment](https://developer.github.com/apps/quickstart-guide/setting-up-your-development-environment)" quickstart guide on developer.github.com. ## Install From 7cad7d91ba1b1f8658b56911d449c195c4c4f440 Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Mon, 17 Dec 2018 15:34:27 -0600 Subject: [PATCH 14/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7da0ac7..81f494b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -This is an example GitHub App that adds a label to all new issues opened in a repository. You can follow the "[Using the GitHub API in your app](https://developer.github.com/apps/quickstart-guide/using-the-github-api-in-your-app)" quickstart guide on developer.github.com to learn how to build the app code in `server.rb`. +This is an example GitHub App that adds a label to all new issues opened in a repository. You can follow the "[Using the GitHub API in your app](https://developer.github.com/apps/quickstart-guides/using-the-github-api-in-your-app/)" quickstart guide on developer.github.com to learn how to build the app code in `server.rb`. This project listens for webhook events and uses the Octokit.rb library to make REST API calls. This example project consists of two different servers: * `template_server.rb` (GitHub App template code) * `server.rb` (completed project) -To learn how to set up a template GitHub App, follow the "[Setting up your development environment](https://developer.github.com/apps/quickstart-guide/setting-up-your-development-environment)" quickstart guide on developer.github.com. +To learn how to set up a template GitHub App, follow the "[Setting up your development environment](https://developer.github.com/apps/quickstart-guides/setting-up-your-development-environment/)" quickstart guide on developer.github.com. ## Install From 870e1990bb896fc8a8fbf530cea64913657c0549 Mon Sep 17 00:00:00 2001 From: Nicholas Bishop Date: Sun, 14 Jul 2019 15:26:42 -0400 Subject: [PATCH 15/17] Fix typos --- server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.rb b/server.rb index f75b66d..8a40db4 100644 --- a/server.rb +++ b/server.rb @@ -78,7 +78,7 @@ def get_payload_request(request) # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, - # so GitHub can be sure that it came from the app an not altererd by + # so GitHub can be sure that it came from the app and was not altered by # a malicious third party. def authenticate_app payload = { From 9417f957cb3976cbdf4df32c389b125c239abc97 Mon Sep 17 00:00:00 2001 From: David Porcel Date: Mon, 12 Aug 2019 11:02:38 +0200 Subject: [PATCH 16/17] Fix typos --- template_server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template_server.rb b/template_server.rb index 5a945d0..f913aa1 100644 --- a/template_server.rb +++ b/template_server.rb @@ -89,7 +89,7 @@ def get_payload_request(request) # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, - # so GitHub can be sure that it came from the app an not altererd by + # so GitHub can be sure that it came from the app and wasn't alterered by # a malicious third party. def authenticate_app payload = { From 7e1ba44288d9c9d1618299b21cb56f4577e4206f Mon Sep 17 00:00:00 2001 From: killer-lab <53106075+killer-lab@users.noreply.github.com> Date: Thu, 9 Jan 2020 23:13:15 -0800 Subject: [PATCH 17/17] Add files via upload --- using-the-github-api-in-your-app-master.zip | Bin 0 -> 7544 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 using-the-github-api-in-your-app-master.zip diff --git a/using-the-github-api-in-your-app-master.zip b/using-the-github-api-in-your-app-master.zip new file mode 100644 index 0000000000000000000000000000000000000000..af16319d2668b06a7a9c12853c7952066781ceef GIT binary patch literal 7544 zcmbW6byVC-vd1U5ySr;}myqDWB|wni3^Ksrt_eW`3QmL-r@N}YstT}hD1bi(IuKjsA0PkxVFDlnT%4@H7HrOzKsF02 zXG<3oHe-7$HY+fjhn_D(98_?a@*4_sAmuaU9v|}WfP)ww6XijJjkPBBxm8SZtV{nKt0$hhzeeL%^ z_j-?dgqgOt?fPk=H@BbH0`$`YUP;>20si#rN09Tmm=ib;9{n-7f5)qbHL$V(+d2O9 z32bKuG_B)r#M)YJ}9aTvQ74L$iLiNh2h2@592w(Q3%DEaKRhj-NOq%MH{DH)!Cr8=aMYc z_H~r=b!AYThx^D+-%Q5`RZK(uYe|0isk{*Y1wH}*Mqz$$qyFh9yN#Wx^12(68zeO&?0e9QE%zKG6Wic2iH0zY++)fU_1xMuF-u2Cc3=DgR?=Yk97R9e7L zg+v9Hx13JIwNH~FWzA#U+U2>XZ0TMaE-g#|Np~u;_!Kj2ce~`C3kzB1JamliJKpsm z!xg-RBPxY!`BWe&kspI#u7I1FIpK2!Rz)4>JsyQR^Cx zo+};{fgqZK>cJzFlUZ$R-;)AJvZDIT0apk+i3^tQ(1BJY2lPb1$7ClT3#AQMi`3TH z&O4r2hp2IHPOky~#T)WwXU*!o+;!{D2Ya9mAeLWjVXMqp*9CVA`>*HhQEW=Q>BHU~ z!vO$Xzj4mgrNyO`q}grF{@T1W4JA8VF09X+FT-bH(fhzK^KC`mNRBXk@W`4WXCE1L zaG|z)4ia}h-L9i>v8d05w0Dy(76-hBe4sE#Dnut0{7 zrP0mAEzuF&=J9C|$}rI7RfDWWg-P*rnxQKxkS|<&eIQ#mv_Si_BYLLFp-tNUO=-}= z+?KfD#ce*Lqp?_;{Cu{#jUg2Oth`i2M6r%W(NB8976w^mLYU>9F$pfHvrx0B^zDl{ zZcT}e%9IG`@;H1QYRPK_GAjv48|hXI2-Q|Y*St3zE^R}LU>@#N4}5C-B2wnUDr{&j zV;b8MA4zGs^~qk@e(3@Lmsnme9t)O(t<`6z2aLxBS5swh>61;`fv|FL`%Mg*zXl8@ z;l3NmpjEL@GGcKxg0HHing9i+dt_Roo{TtIBGt)Z-3cNGr8tFQUU*bE>*j!rJnl1SaAid%dA2xm9}UT-DNk}qgvOB&to2?HveK)p`{LI5wH zfJQKYuS{KW9EiRV%5zf@q{ke_#(1Ber(J9-F~95^ozb$oK!ig z`XV{MmPs66z6vCApMpIvCPS%C^xoBqIQ&%8xHrBdE+pBgD4dF;LiO$tRJ9@GMQV8m z;!inFjr{ZI{rgmU+D+_iQ64Bd(d zfR2adLE=$SK#&WGi}S4%P)lEK&NG^owQu)UBF{sr;gp4Ktd8@og?h0gv%JDoQT%oZ zmbv6vgBmbpUp}@A5Bu06VG)ImgT8h;Fq4!^kI6+^W++Q3Pdcn0az>ZFSJrkr+V{9R zQ)yd!HU=aUTs#rf(U%v(PuJZ>lB}q*4UI%Z%j{lr&3~WA$px`{`2~`ZKS^F%)fj~1Og~xm z9DVTNp#ZMkS2>qZgktnY4|PGe)VF~k(|Hk*jK(L8M2s9=&7&d8*g4y$8}{z!5+%Xy zC)>to^z~~~c2A|cW5QwtRALCkPr1@lK$8$()*P|@8?A#=&&6;<&AgTEI$kZyDBP6K zCE`^t@%4E`CnJ)0gsI>rt(q`W%y+T8`PUXMw|lp@ z!_0)mgeeVMqCfS`1&_!=Y5Ny48M*145C}MnimaztUE}fnpA8G0Ba~SyMsJXT%`3Gp zS7vm$J@|cnxIDy&GnTNMe5R40yk##ZpZhx#iN27*YLA%NjEjj5vsNp_m&JaY#KN&b zb|v0E`Do)qJC(~c>ZuRH<5)_J3`j*1w`%-$tGHl9QcZQVk&GxFe z1?0xLx{On0k*=7>QNhwHV??!D!m{g`O&{gonn^GT&ZZ?_H_mk|Fp94j^eeE!8;Y9c zuEphrXmGO1Wc!3Dv$GFHU&tYSzhr}n&_2;eOnpug(682=fa@Vw{RLAkxtw4`mINZx zq!`-R^n4x27$J5X{B8`SV0%}#b7pukoYM*z;?ayp)}Z{T8)ubNiv=%gMv~*YO+_EA2z(`U%RtA46gVCnS zuG#O_PtLZvN-h~o(WP}~*$kh@+xrfnx&eBA7ml6jGWAO&ef&XYLUF?y<53%z5PYo_ zsE+FBzDaEYZgUl3KZvI#YqT&=m0msf3k^K#ozf_O`Lumxb&d^@KHN!3ZS_3iOtx;= z7knrFZQq)6Iemc&thg@qGO`8y)JYSI!a^YIc`_2*UWA6x>-<=r@@$3Jz-m^W`K9htT!rdL8UYwbEk4HL;x=LSZ^&5mC)Et)OHVZ~S9a|GW#PAYW z@V0h7(#$I@G->)3Ttt$y#cIOeCDPajCiN&#KXN|K=%!*jmCbKiB(z8B|CUE2=58Au0 z@3KX{<9WTJ$QS|c>02z$SyK9V2Row)uJK5pJfzpTU&+5me+72A6G;O##PoOk{z^s$Z4Ny#hPtR^2fXbr=U(*tazKegT-Ln7Q89r$H;6 zJI1uBKp!f{XVWF`D@7Z%7-vkp`nugUph6U=PC@aABRvqd(umQHsVw;fy*a+P4}m;T zuP<|iT+R2fcANaGSl1kK+*?1APF@EecL;T9_+wG4O6g`}LK2?HeP8wGrAn2ohh7Kl z=2;(Ty6Mx2G8MdsM;M_Pkuc4HwSya?dw|wa+qByXSHqb$TXRv5X9bQFm-yf01?hvU zObNDIx0XjDpWfcM7l314yk-g%Bm6|HEh_t>T^aG}`fR8s^6ZPP!SFV=4i#ANt;O-`IYZVSAn8+M?#&yy+JA zq^5YBxNLNXWbkpL-|`g|+8c01WR2kE=Ri)|;|kTUD{0|g83F8{3OS6x8%nV+F4$@B zBk0eeny%+Z;;P)#P=^`)j^*=iYb~CkCoLEmnO){^^Chng-9R-9Ps0>+ZgTV)+B5V$ zBk~riEdlAkzHpxWmU9SBH>3xck4@5&OLk&l?$AJ71=y!&WNMz<76}r4aq`$LVjZ%b zq;N)>cShok&&GPbfp%SEF%ZX;Uw}`-P!M4H?(~@txiI)CS}n;X3O>QDf)0{fJl!xO zN4K2%5+KW5K!9+^GiK-gbxVui-I?7}?qsphY>_u{c^RZFA@4{^-(yhmhhe^UcOE>iy*DHye(oKVk=_3z7~S9 zOmlL2LjE*Esn`1=yx(fniRGd+1MOv`*j_vxDH;>#<4ZT2?Oi>mnxYA_%rFR%7|5l6 zcs&VGbp-42W!}lL@5k@FEDmFs%X?fhNV_^L_Bmcg3BR_sE;lZ9pt^~=*!)XAZ$&F` zgC#n2ueRg1tw=-g3ySc|gY48V$BuRM4j}3rw%$7D_@Eqx@7*zr)WmhXqi+K3?6rNLNpjb4*sJy|Ng?s~DY^#|_vkWoO2RA15GYCN7?iZj>&G2Bwz^XD5=JBkd*kmf}#_X9S7;wTg&#o4HvlJsP1`7>#_ z#V4b^-b#U8YFxX~H;H|D?b0jHzPcnmSBFo1zsYS%9?V{$!9J77H{O}h!<`&2-N#N) z=eTP}lAr68w1`ETg^GL!gJB$zS&uKxQc0I+$58%qx?!jxEG|ziuDrxGA%FJ0p4B<# z=-|0S+7@R%Zc^x|e1^WwlKh6yL^*zJ7)^jjl@yH^MJrVXUb)3L!TfTDGEMt#vWpYj z^tRRz-@Jtly!yDVK6VjE%UgI|mfV49LYaK^1qsWjkATj`09QPH5dx#@q=E!gIZs#KEu@E7KqD{46 z0U~9>a}PcEgg|yk{f1d%lr=`r#4@~y1@$Y%g8FBQuQZ{7uf$pLL`Ejgj?RwV-;0tq z=F!$mMMcUCU*8s6I4&F-Mn;&<+hQ6t`4XmKb`wICqY=bU#nNXSMTVTT7}U7jBd1lz0Wx*|2;z`v<*xO`)7%7o`j`xMLh@LcbJ!+Llig-{|Gj@z5O zDXl~jzq9u2WO;q{+czCsN?yXL?10r z<}70S1{mWHVMWPVt?JDV{^1~pgp)zmFK%^f;#a=+Hum3k$Tll_bmNY2g*KGwq=hf| z=&^}vQ+d|fU#raLvJjoTM#Is415zK6YS>g`^JLJkz_X^{k~5mYSR(h!3bnv#OSEBx zrwr}Vc)mP7d23w04L2yiNjJqH*;4zx4BsqsTYaU&Vh0bQF&Xi=P}ISy!Iz2;($jO z!zhN3^Rv;D--fLRxwB{}R|z}@b5&Nd!0g|ud~-tFT741IyZQMUUBH#t3=0QZos=pE z#Ww9uC(YT4^Fngaqut!kU8C zqA*^OxKp79Y3wi0f^oY6^@foY_eXR)yfgitA$#V|gdqW&Hdr0Z`!QMTs_IbwuyOY~Ah`3b z_D#}r(afdKJ*nTTzYfw~o{m_KO?$MO4gd)tG^N*133n6;_72Tc3Sq|120Ng5hSsFl z4I^+jr8xG4m98m9!q2J~E)n^+`G+^#hDd4qOwBbl#}N~+cFe55B4(nsa-wW^7IJc; zx^_X(F^ab;oW*6gFEA~gWiI{{LJTI$7 zj`}Acr}F1k!$X}bBP(l%P4_Fk22U7ewMm=nPp1@yADz=SW01MuM9BrKEfpR@-&mXA z#?FX6q-8akQUo=J*I_1r;5 zXJFs`f=$edbw;|0^%*S*@-Vk%AAXGKtyI{W_&RH)C%P0dz;MUjE1>HAEGoOIFXG4< z8nFgXVuQ?am-ANzpFD_805o5^w<%QvswKe~x}EF1a*JIl<&oKZ{-nmv>rN@{P?d@7 zK=nnOf?qtHO7Jk^Kawz>#5x0}uQ=2iN<(tdEPVJHIr*tm9kdpK)^ z`DZsfQ~G=HETzXpPn%Yz#7S-67c8CY?k(RxG<6?6G;#mC9P`j3dl*f%kV~`c;4V zmGY}z@{@A;`;>oGPku%H+S>n#s)hag0sVWU|0m}^_hEl>oDlv#=ij@sKRG`(=-1Bc zPtNvlaQ?jj`IYl)jq{VUiTU^A`9CTiRR#D5hXD`e6EdLmVJjfS000w!lb>J6jL(Eu kz|7dx#F*2}oR`y-QxIq@B*e|dC&dC&0=7Kc?>$#{d8T literal 0 HcmV?d00001