Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Qu4tro committed Sep 14, 2019
0 parents commit a2bec32
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
@@ -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
28 changes: 28 additions & 0 deletions .travis.yml
@@ -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 'moc.liamg@ocsicnarf.n.reivax' | 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
21 changes: 21 additions & 0 deletions LICENSE
@@ -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.
86 changes: 86 additions & 0 deletions README.md
@@ -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
11 changes: 11 additions & 0 deletions shard.yml
@@ -0,0 +1,11 @@
name: slack-events-api
version: 0.1.0
crystal: 0.30.1

authors:
- Xavier Francisco <echo "moc.liamg@ocsicnarf.n.reivax" | rev>

description: |
The middlewares you need to deal with Slack Events API
license: MIT
65 changes: 65 additions & 0 deletions spec/challenge_handler_spec.cr
@@ -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
48 changes: 48 additions & 0 deletions spec/spec_helper.cr
@@ -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
84 changes: 84 additions & 0 deletions spec/verification_handler_spec.cr
@@ -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
2 changes: 2 additions & 0 deletions src/slack-events-api.cr
@@ -0,0 +1,2 @@
require "./slack-events-api/challenge_handler"
require "./slack-events-api/verification_handler"

0 comments on commit a2bec32

Please sign in to comment.