-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a2bec32
Showing
11 changed files
with
493 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/lib/ | ||
/bin/ | ||
/.shards/ | ||
*.dwarf | ||
|
||
# Libraries don't need dependency lock | ||
# Dependencies will be locked in applications that use them | ||
/shard.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
language: crystal | ||
script: | ||
- crystal spec | ||
- crystal tool format --check | ||
- crystal docs | ||
|
||
|
||
before_deploy: | ||
- git config --local user.name "Xavier Francisco" | ||
- git config --local user.email "<echo '[email protected]' | rev>" | ||
- if [[ "$TRAVIS_BRANCH" = "releases" ]] ; then git tag "$TRAVIS_COMMIT_MESSAGE"; fi | ||
|
||
deploy: | ||
- provider: pages | ||
skip_cleanup: true | ||
github_token: $GITHUB_TOKEN | ||
project_name: crystal-slack-events-api | ||
on: | ||
branch: master | ||
local_dir: docs | ||
|
||
- provider: releases | ||
api_key: | ||
secure: 077LR+xGo8Ott2b+i2FRcSlHSdCuhf3wtW9iUmeJyvTDXvMe21pNjHyDzwPPGe2i81sqvwkddJxEG6r1KmSWVwSiSi4UCiyuDf7C38fUy3kp/lBActy4jymm5Nipk+Egu1aIiR/wDRQ1k1z7cC3sQPCvTLuA5SLie4KyX/d0r44ofG4yIHBsIaRnOtcm6D7ZYMKNiFJ6Cc3hv+aBPRvRFmG+yRtyr+qja0FjyAPvV3R0/beGbwymbWYfX3VuejUsqq42TlK3IkPT6D9o6t1BF8RkAHnG90TNmw6Fd+4p27MJ+KNryxmz9IYoU++oHsxhecKYmSbedKGUZHwAjHIsLJW3fbQIy+nQyzeiMxMmp0k3UrR2EFBVxAg3M1Unw9vhBtskKO39nw23tI2bjqSqfWdVCyOvTw54V5jTwuW8Klv19qEe9m1kjPrLUQvlMPjUclqm1g/dKwbqcFtztlwBu2JyDPutjRwpeJSun0NpEg0qtZU0DJssnNOAShQnoXDKIpyPESTaT0MU09Xj1xlVrzxQy5WK34ZSLz8FzP3KMXC7eKVaJuwjpJRw9kkWPPj6px3DHo3hn2e0OS0fLpkeZ9CWkYGzmS6ADCt33idicRgXT7xPn0YOKn/zjpu8WwXssMwCuy46+iSpeiXj80dYvd5RJ/htf65re45d2lFqWZk= | ||
file_glob: true | ||
file: "*" | ||
on: | ||
branch: releases |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2019 Xavier Francisco | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
# Slack Events API adapter for Crystal | ||
|
||
[![Build Status](https://travis-ci.com/Qu4tro/slack-events-api.svg?token=Mqsa3fKeSUryp43kNdBt&branch=master)](https://travis-ci.com/Qu4tro/slack-events-api) | ||
|
||
[![Github release](https://img.shields.io/github/release/qu4tro/slack-events-api.svg)](https://github.com/qu4tro/slack-events-api/releases) | ||
|
||
### The middlewares you need to deal with Slack Events API | ||
|
||
## Overview | ||
|
||
slack-events-api is a [Crystal](https://crystal-lang.org/) package composed of two middlewares: | ||
|
||
`SlackEvents::VerificationHandler` | ||
- Middleware that verifies that requests are correctly signed with `SLACK_SIGNING_SECRET` by Slack. All requests going through this middleware, will be checked. In the event of a request whose signature couldn't be verified, the middleware will early return with a `403 - Forbidden`. | ||
- Receives `SLACK_SIGNING_SECRET` as its sole argument. | ||
|
||
|
||
`SlackEvents::ChallengeHandler` | ||
- Middleware that does the initial challenge handshake between Slack and your API. | ||
|
||
Further documentation can be found in https://qu4tro.github.io/slack-events-api/ | ||
|
||
## Installation | ||
|
||
1. Add this to your application's `shard.yml`: | ||
|
||
```yaml | ||
dependencies: | ||
slack-events-api: | ||
github: qu4tro/slack-events-api | ||
``` | ||
2. Run `shards install` | ||
|
||
|
||
## Usage | ||
This example will suffice to perform the initial setup, but actual events will be 404'd, until you write your application-specific handler. | ||
|
||
```crystal | ||
#!/usr/bin/env crystal | ||
require "http/server" | ||
require "http/server/handler" | ||
require "slack-events-api" | ||
middlewares = [ | ||
HTTP::LogHandler.new.as(HTTP::Handler), | ||
HTTP::ErrorHandler.new, | ||
SlackEvents::VerificationHandler.new(ENV["SLACK_SIGNING_SECRET"]), | ||
SlackEvents::ChallengeHandler.new, | ||
] | ||
HTTP::Server.new(middlewares).tap do |server| | ||
address = server.bind_tcp "localhost", ENV["PORT"].to_i | ||
puts "Listening on http://#{address}" | ||
server.listen | ||
end | ||
``` | ||
## Further work | ||
|
||
- Make JSON mappings for all event types supported by the Event API | ||
- If a reverse-proxy middleware comes up for Crystal, I think it's worth thinking about creating a docker image, to allow for the verification and challenge-setup to be automated for any ad-hoc server. | ||
|
||
|
||
## Development | ||
|
||
Any restriction to development should be tool-automated. | ||
So, feel free to open PRs. If all the tests pass, it should be good to merge, if it fits the package domain - opening an issue is a good way to clarify. In fact, feel free to open issues for any type of clarification. | ||
|
||
|
||
## Contributing | ||
|
||
1. [Fork it](https://github.com/Qu4tro/slack-events-api/fork) | ||
2. Create your feature branch (`git checkout -b my-new-feature`) | ||
3. Commit your changes (`git commit -am 'Add some feature'`) | ||
4. Push to the branch (`git push origin my-new-feature`) | ||
5. Create a new Pull Request | ||
|
||
|
||
## Authors | ||
|
||
* **Xavier Francisco** - *Initial work* - [Qu4tro](https://github.com/Qu4tro) | ||
|
||
## License | ||
|
||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
name: slack-events-api | ||
version: 0.1.0 | ||
crystal: 0.30.1 | ||
|
||
authors: | ||
- Xavier Francisco <echo "[email protected]" | rev> | ||
|
||
description: | | ||
The middlewares you need to deal with Slack Events API | ||
license: MIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
require "spec" | ||
|
||
require "./spec_helper" | ||
require "../src/slack-events-api/challenge_handler" | ||
|
||
def cspec | ||
SpecHandler.new SlackEvents::ChallengeHandler.new | ||
end | ||
|
||
good_challenge = JSON.build do |json| | ||
json.object do | ||
json.field "token", "doesntmatter" | ||
json.field "challenge", "challenge-string" | ||
json.field "type", "url_verification" | ||
end | ||
end | ||
|
||
wrong_type = JSON.build do |json| | ||
json.object do | ||
json.field "token", "doesntmatter" | ||
json.field "challenge", "challenge-string" | ||
json.field "type", "not_url_verification" | ||
end | ||
end | ||
|
||
wrong_schema = JSON.build do |json| | ||
json.object do | ||
json.field "name", "foo" | ||
json.field "values" do | ||
json.array do | ||
json.number 1 | ||
json.number 2 | ||
json.number 3 | ||
end | ||
end | ||
end | ||
end | ||
|
||
describe SlackEvents::ChallengeHandler do | ||
describe "#call" do | ||
it "replies to the challenge with the challenge key" do | ||
request = simple_post(good_challenge) | ||
cspec.with request do |response| | ||
response.status_code.should eq 200 | ||
response.headers["Content-Type"]?.should eq "text/plain" | ||
response.body.should eq "challenge-string" | ||
end | ||
end | ||
|
||
it "does nothing if challenge payload can't be parsed" do | ||
request = simple_post(":o") | ||
cspec.passthrough?(request).should be_true | ||
end | ||
|
||
it "does nothing if type is not a match " do | ||
request = simple_post(wrong_type) | ||
cspec.passthrough?(request).should be_true | ||
end | ||
|
||
it "does nothing if type is not a match " do | ||
request = simple_post(wrong_schema) | ||
cspec.passthrough?(request).should be_true | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
class SpecHandler | ||
def initialize(@handler : HTTP::Handler) | ||
end | ||
|
||
def handler | ||
@handler | ||
end | ||
|
||
def with(request, &block) | ||
io = IO::Memory.new | ||
response = HTTP::Server::Response.new(io) | ||
context = HTTP::Server::Context.new(request, response) | ||
|
||
@handler.call(context) | ||
response.close | ||
|
||
io.rewind | ||
yield HTTP::Client::Response.from_io(io) | ||
end | ||
|
||
def make_request(request) | ||
self.with request do |r| | ||
end | ||
end | ||
|
||
def passthrough?(request) | ||
not_found? request | ||
end | ||
|
||
def not_found?(request) | ||
self.with request do |response| | ||
not_found = response.status_code == 404 | ||
not_found &&= response.headers["Content-Type"]? == "text/plain" | ||
not_found &&= response.body == "Not Found\n" | ||
not_found | ||
end | ||
end | ||
|
||
def forbidden?(request, message = nil) | ||
self.with request do |response| | ||
return response.status_code == 403 | ||
end | ||
end | ||
end | ||
|
||
def simple_post(body, headers = nil) | ||
HTTP::Request.new("POST", "/", headers, body) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
require "spec" | ||
|
||
require "./spec_helper" | ||
require "../src/slack-events-api/verification_handler" | ||
|
||
def vspec | ||
SpecHandler.new SlackEvents::VerificationHandler.new("secret") | ||
end | ||
|
||
def signed_post(timestamp = Time.utc.to_unix.to_s, signature = nil) | ||
headers = HTTP::Headers{ | ||
"X-Slack-Signature" => signature || "", | ||
"X-Slack-Request-Timestamp" => timestamp, | ||
} | ||
|
||
request = simple_post("doesntmatter", headers: headers) | ||
|
||
if signature == nil | ||
request.headers["X-Slack-Signature"] = | ||
vspec.handler | ||
.as(SlackEvents::VerificationHandler) | ||
.computed_signature(request) | ||
end | ||
|
||
request | ||
end | ||
|
||
describe SlackEvents::VerificationHandler do | ||
describe "passing #call" do | ||
it "allows the request through if the signature is correct" do | ||
vspec.passthrough?(signed_post).should be_true | ||
end | ||
|
||
it "gives the request some leeway if the signature is outdated" do | ||
past = Time.utc_now - Time::Span.new(hours: 0, minutes: 4, seconds: 0) | ||
past_ts = past.to_unix.to_s | ||
vspec.forbidden?(signed_post timestamp: past_ts).should be_false | ||
end | ||
|
||
it "gives the request some leeway if the signature is from the future" do | ||
future = Time.utc_now + Time::Span.new(hours: 0, minutes: 4, seconds: 0) | ||
future_ts = future.to_unix.to_s | ||
vspec.forbidden?(signed_post timestamp: future_ts).should be_false | ||
end | ||
end | ||
|
||
describe "blocing #call" do | ||
it "blocks the request if the signature is outdated" do | ||
past = Time.utc_now - Time::Span.new(hours: 0, minutes: 10, seconds: 0) | ||
past_ts = past.to_unix.to_s | ||
vspec.forbidden?(signed_post timestamp: past_ts).should be_true | ||
end | ||
|
||
it "blocks the request if the signature is from the future" do | ||
future = Time.utc_now + Time::Span.new(hours: 0, minutes: 10, seconds: 0) | ||
future_ts = future.to_unix.to_s | ||
vspec.forbidden?(signed_post timestamp: future_ts).should be_true | ||
end | ||
|
||
it "blocks the request if the signature is wrong" do | ||
request1 = signed_post signature: ":o" | ||
request2 = signed_post signature: "itsme" | ||
request3 = signed_post signature: "letmein" | ||
request4 = signed_post signature: "iforgotthesecretknock" | ||
|
||
vspec.forbidden?(request1).should be_true | ||
vspec.forbidden?(request2).should be_true | ||
vspec.forbidden?(request3).should be_true | ||
vspec.forbidden?(request4).should be_true | ||
end | ||
end | ||
|
||
describe "#computed_signature" do | ||
it "computes a signture correctly" do | ||
signature = "v0=3de8edba6fa2f065b575537fba33bd4c4217aa3d649c7852dd831e2b8caff0ad" | ||
request = signed_post(signature: signature, timestamp: "1234567890") | ||
|
||
vspec.handler | ||
.as(SlackEvents::VerificationHandler) | ||
.computed_signature(request) | ||
.should eq signature | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
require "./slack-events-api/challenge_handler" | ||
require "./slack-events-api/verification_handler" |
Oops, something went wrong.