From 6b16d07d807f97a742801c7ea54cb4fac1f1fb39 Mon Sep 17 00:00:00 2001 From: Deon George Date: Tue, 22 Feb 2022 11:52:55 +1100 Subject: [PATCH] Incorporate HTTP endpoint logic so we can now do websockets or HTTP endpoints --- src/Base.php | 21 +++++- src/Client/Payload.php | 4 +- src/Event/Factory.php | 16 ++-- src/Http/Controllers/EventsController.php | 30 ++++++++ src/Http/Controllers/SlackAppController.php | 8 +- src/Http/Middleware/CheckRequest.php | 81 +++++++++++++++++++++ src/Http/Middleware/CheckSignature.php | 69 ++++++++++++++++++ src/Models/Channel.php | 3 +- src/Models/Enterprise.php | 3 +- src/Models/Team.php | 3 +- src/Models/Token.php | 4 +- src/Models/User.php | 5 +- src/Providers/SlackServiceProvider.php | 3 + src/Traits/ScopeActive.php | 17 +++++ src/config/slack.php | 3 + src/routes.php | 34 ++++++--- 16 files changed, 264 insertions(+), 40 deletions(-) create mode 100644 src/Http/Controllers/EventsController.php create mode 100644 src/Http/Middleware/CheckRequest.php create mode 100644 src/Http/Middleware/CheckSignature.php create mode 100644 src/Traits/ScopeActive.php diff --git a/src/Base.php b/src/Base.php index adf32ad..529396a 100644 --- a/src/Base.php +++ b/src/Base.php @@ -6,6 +6,11 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Slack\Models\{Channel,Enterprise,Team,User}; +use App\Models\Channel as AppChannel; +use App\Models\Enterprise as AppEnterprise; +use App\Models\Team as AppTeam; +use App\Models\User as AppUser; + /** * Class Base - is a Base to all incoming Slack POST requests * @@ -43,7 +48,9 @@ abstract class Base */ final public function channel(bool $create=FALSE): ?Channel { - $o = Channel::firstOrNew( + $class = class_exists(AppChannel::class) ? AppChannel::class : Channel::class; + + $o = $class::firstOrNew( [ 'channel_id'=>$this->channel_id, ]); @@ -59,7 +66,9 @@ abstract class Base final public function enterprise(): Enterprise { - return Enterprise::firstOrNew( + $class = class_exists(AppEnterprise::class) ? AppEnterprise::class : Enterprise::class; + + return $class::firstOrNew( [ 'enterprise_id'=>$this->enterprise_id ]); @@ -73,7 +82,9 @@ abstract class Base */ final public function team(bool $any=FALSE): ?Team { - $o = Team::firstOrNew( + $class = class_exists(AppTeam::class) ? AppTeam::class : Team::class; + + $o = $class::firstOrNew( [ 'team_id'=>$this->team_id ]); @@ -94,7 +105,9 @@ abstract class Base */ final public function user(): User { - $o = User::firstOrNew( + $class = class_exists(AppUser::class) ? AppUser::class : User::class; + + $o = $class::firstOrNew( [ 'user_id'=>$this->user_id, ]); diff --git a/src/Client/Payload.php b/src/Client/Payload.php index d415340..0ab06a8 100644 --- a/src/Client/Payload.php +++ b/src/Client/Payload.php @@ -17,9 +17,9 @@ class Payload implements \ArrayAccess, \JsonSerializable * * @param array $data The payload data. */ - public function __construct(array $data) + public function __construct(array $data,bool $key=FALSE) { - $this->data = $data; + $this->data = $key ? ['payload'=>$data ] : $data; } /** diff --git a/src/Event/Factory.php b/src/Event/Factory.php index a2817c0..c2e088a 100644 --- a/src/Event/Factory.php +++ b/src/Event/Factory.php @@ -15,14 +15,14 @@ class Factory { * @var array event type to event class mapping */ public const map = [ - 'app_home_opened'=>AppHomeOpened::class, - 'member_joined_channel'=>MemberJoinedChannel::class, - 'channel_left'=>ChannelLeft::class, - 'group_left'=>GroupLeft::class, - 'message'=>Message::class, - 'reaction_added'=>ReactionAdded::class, - 'pin_added'=>PinAdded::class, - 'pin_removed'=>PinRemoved::class, + 'app_home_opened' => AppHomeOpened::class, + 'member_joined_channel' => MemberJoinedChannel::class, + 'channel_left' => ChannelLeft::class, + 'group_left' => GroupLeft::class, + 'message' => Message::class, + 'reaction_added' => ReactionAdded::class, + 'pin_added' => PinAdded::class, + 'pin_removed' => PinRemoved::class, ]; /** diff --git a/src/Http/Controllers/EventsController.php b/src/Http/Controllers/EventsController.php new file mode 100644 index 0000000..8ecdeb3 --- /dev/null +++ b/src/Http/Controllers/EventsController.php @@ -0,0 +1,30 @@ +all(),TRUE)); + Log::info(sprintf('%s:Dispatching Event [%s]',static::LOGKEY,get_class($event))); + event($event); + + return response('Event Processed',200); + } +} diff --git a/src/Http/Controllers/SlackAppController.php b/src/Http/Controllers/SlackAppController.php index c35ea20..dd29500 100644 --- a/src/Http/Controllers/SlackAppController.php +++ b/src/Http/Controllers/SlackAppController.php @@ -14,7 +14,7 @@ use Slack\Models\{Enterprise,Team,Token,User}; class SlackAppController extends Controller { - private const LOGKEY = 'CSA'; + protected const LOGKEY = 'CSA'; private const slack_authorise_url = 'https://slack.com/oauth/v2/authorize'; private const slack_oauth_url = 'https://slack.com/api/oauth.v2.access'; @@ -39,7 +39,7 @@ class SlackAppController extends Controller public function home() { - return sprintf('Hi, for instructions on how to install me, please reach out to @deon.'); + return sprintf('Hi, for instructions on how to install me, please reach out to %s.',config('slack.app_admin','Your slack admin')); } public function setup() @@ -54,7 +54,7 @@ class SlackAppController extends Controller * @return string * @throws \GuzzleHttp\Exception\GuzzleException */ - public function install(Request $request) + public function install(Request $request,bool $oauth=FALSE) { if (! config('slack.client_id') OR ! config('slack.client_secret')) abort(403,'Slack ClientID or Secret not set'); @@ -158,7 +158,7 @@ class SlackAppController extends Controller $so->admin_id = $uo->id; $so->save(); - return sprintf('All set up! Head back to your slack instance %s.',$so->description); + return $oauth ? $output : sprintf('All set up! Head back to your slack instance %s.',$so->description); } /** diff --git a/src/Http/Middleware/CheckRequest.php b/src/Http/Middleware/CheckRequest.php new file mode 100644 index 0000000..951450d --- /dev/null +++ b/src/Http/Middleware/CheckRequest.php @@ -0,0 +1,81 @@ +path()),['m'=>__METHOD__]); + + // For app installs, we have nothing to check. + if (in_array($request->path(),config('slack.bypass_routes'))) + return $next($request); + + switch ($request->path()) { + // For slashcmd full validation is done in the controller + case 'api/slashcmd': + return $next($request); + + case 'api/event': + // URL Verification + if ($request->input('type') === 'url_verification') { + Log::debug(sprintf('%s:Responding directly to URL Verification',static::LOGKEY),['m'=>__METHOD__,'r'=>$request->all()]); + return response($request->input('challenge'),200); + } + + $event = EventFactory::make(new Payload($request->all(),TRUE)); + break; + + case 'api/imsgopt': + $event = OptionsFactory::make($request); + break; + + case 'api/imsg': + $event = InteractiveFactory::make($request); + break; + + default: + // Quietly die if we got here. + return response('',444); + } + + // Ignore events for inactive workspaces + if ($event->enterprise_id AND (! $event->enterprise()->active)) { + Log::notice(sprintf('%s:IGNORING post, Enterprise INACTIVE [%s]',static::LOGKEY,$event->enterprise_id),['m'=>__METHOD__]); + + // Quietly die if the team is not active + return response('',200); + + } elseif ((! $event->enterprise_id) AND ((! $event->team()) OR (! $event->team()->active))) { + Log::notice(sprintf('%s:IGNORING post, Team INACTIVE [%s]',static::LOGKEY,$event->team_id),['m'=>__METHOD__]); + + // Quietly die if the team is not active + return response('',200); + + } else { + Log::debug(sprintf('%s:Incoming Request Allowed',static::LOGKEY),['m'=>__METHOD__,'e'=>$event->enterprise_id,'t'=>$event->team_id,'eo'=>$event->enterprise()->id,'to'=>$event->team()]); + + return $next($request); + } + } +} \ No newline at end of file diff --git a/src/Http/Middleware/CheckSignature.php b/src/Http/Middleware/CheckSignature.php new file mode 100644 index 0000000..d4fbdb8 --- /dev/null +++ b/src/Http/Middleware/CheckSignature.php @@ -0,0 +1,69 @@ +path(),config('slack.bypass_routes'))) { + // get the remote sign + $remote_signature = $request->header('X-Slack-Signature'); + Log::info(sprintf('%s:Incoming request - check slack SIGNATURE [%s]',static::LOGKEY,$remote_signature),['m'=>__METHOD__]); + + // Load the secret, you also can load it from env(YOUR_OWN_SLACK_SECRET) + $secret = config('slack.signing_secret'); + + $body = $request->getContent(); + + // Compare timestamp with the local time, according to the slack official documents + // the gap should under 5 minutes + // @codeCoverageIgnoreStart + if (! $timestamp = $request->header('X-Slack-Request-Timestamp')) { + Log::alert(sprintf('%s:No slack timestamp - aborting...',static::LOGKEY),['m'=>__METHOD__]); + + return response('',444); + } + + if (($x=Carbon::now()->diffInMinutes(Carbon::createFromTimestamp($timestamp))) > 5) { + Log::alert(sprintf('%s:Invalid slack timestamp [%d]',static::LOGKEY,$x),['m'=>__METHOD__]); + + return response('',444); + } + // @codeCoverageIgnoreEnd + + // generate the string base + $sig_basestring = sprintf('%s:%s:%s',Base::signature_version,$timestamp,$body); + + // generate the local sign + $hash = hash_hmac('sha256',$sig_basestring,$secret); + $local_signature = sprintf('%s=%s',Base::signature_version,$hash); + + // check two signs, if not match, throw an error + if ($remote_signature !== $local_signature) { + Log::alert(sprintf('%s:Invalid slack signature [%s]',static::LOGKEY,$remote_signature),['m'=>__METHOD__]); + + return response('',444); + } + } + + return $next($request); + } +} diff --git a/src/Models/Channel.php b/src/Models/Channel.php index 6459054..469696c 100644 --- a/src/Models/Channel.php +++ b/src/Models/Channel.php @@ -4,14 +4,13 @@ namespace Slack\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; -use Leenooks\Traits\ScopeActive; +use Slack\Traits\ScopeActive; class Channel extends Model { use ScopeActive; protected $fillable = ['team_id','channel_id','name','active']; - protected $table = 'slack_channels'; /* RELATIONS */ diff --git a/src/Models/Enterprise.php b/src/Models/Enterprise.php index e7bb636..fc057c8 100644 --- a/src/Models/Enterprise.php +++ b/src/Models/Enterprise.php @@ -3,14 +3,13 @@ namespace Slack\Models; use Illuminate\Database\Eloquent\Model; -use Leenooks\Traits\ScopeActive; +use Slack\Traits\ScopeActive; class Enterprise extends Model { use ScopeActive; protected $fillable = ['enterprise_id']; - protected $table = 'slack_enterprises'; /* RELATIONS */ diff --git a/src/Models/Team.php b/src/Models/Team.php index 4eb5a7f..18d4aca 100644 --- a/src/Models/Team.php +++ b/src/Models/Team.php @@ -3,15 +3,14 @@ namespace Slack\Models; use Illuminate\Database\Eloquent\Model; -use Leenooks\Traits\ScopeActive; use Slack\API; +use Slack\Traits\ScopeActive; class Team extends Model { use ScopeActive; protected $fillable = ['team_id']; - protected $table = 'slack_teams'; /* RELATIONS */ diff --git a/src/Models/Token.php b/src/Models/Token.php index bd49c22..9dec68f 100644 --- a/src/Models/Token.php +++ b/src/Models/Token.php @@ -4,14 +4,12 @@ namespace Slack\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; -use Leenooks\Traits\ScopeActive; +use Slack\Traits\ScopeActive; class Token extends Model { use ScopeActive; - protected $table = 'slack_tokens'; - /* RELATIONS */ public function team() diff --git a/src/Models/User.php b/src/Models/User.php index 5053f27..15fdd15 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -4,16 +4,15 @@ namespace Slack\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; -use Leenooks\Traits\ScopeActive; +use Slack\Traits\ScopeActive; class User extends Model { use ScopeActive; - private const LOGKEY = '-MU'; + protected const LOGKEY = '-MU'; protected $fillable = ['user_id']; - protected $table = 'slack_users'; /* RELATIONS */ diff --git a/src/Providers/SlackServiceProvider.php b/src/Providers/SlackServiceProvider.php index 2e50ba2..7414b92 100644 --- a/src/Providers/SlackServiceProvider.php +++ b/src/Providers/SlackServiceProvider.php @@ -9,6 +9,9 @@ use Slack\API; use Slack\Channels\SlackBotChannel; use Slack\Console\Commands\SlackSocketClient; +/** + * @todo For Lumen installs, this service provider is not required? + */ class SlackServiceProvider extends ServiceProvider { /** diff --git a/src/Traits/ScopeActive.php b/src/Traits/ScopeActive.php new file mode 100644 index 0000000..1866a9e --- /dev/null +++ b/src/Traits/ScopeActive.php @@ -0,0 +1,17 @@ +where($this->getTable().'.active',TRUE); + } +} diff --git a/src/config/slack.php b/src/config/slack.php index 1649579..67ddc12 100644 --- a/src/config/slack.php +++ b/src/config/slack.php @@ -6,4 +6,7 @@ return [ 'client_secret' => env('SLACK_CLIENT_SECRET',NULL), 'signing_secret' => env('SLACK_SIGNING_SECRET',NULL), 'register_notification' => env('SLACK_REGISTER_NOTIFICATION',TRUE), + + // Our routes that we dont check for signatures + 'bypass_routes' => ['/','slack-install-button','slack-install'], ]; diff --git a/src/routes.php b/src/routes.php index c878215..90f2249 100644 --- a/src/routes.php +++ b/src/routes.php @@ -4,14 +4,28 @@ $routeConfig = [ 'namespace' => 'Slack\Http\Controllers', ]; -app('router')->group($routeConfig, function ($router) { - $router->get('slack-install-button', [ - 'uses' => 'SlackAppController@button', - 'as' => 'slack-install-button', - ]); +app('router') + ->group($routeConfig, function ($router) { + $router->get('slack-install-button', [ + 'uses' => 'SlackAppController@button', + 'as' => 'slack-install-button', + ]); - $router->get('slack-install', [ - 'uses' => 'SlackAppController@install', - 'as' => 'slack-install', - ]); -}); \ No newline at end of file + $router->get('slack-install', [ + 'uses' => 'SlackAppController@install', + 'as' => 'slack-install', + ]); + + $router->get('', [ + 'uses' => 'SlackAppController@home', + 'as' => 'home', + ]); + }); + +app('router') + ->group(array_merge($routeConfig,['prefix'=>'api']), function ($router) { + $router->post('event', [ + 'uses' => 'EventsController@fire', + 'as' => 'event', + ]); + }); \ No newline at end of file