site_hook 0.6.8 → 0.6.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab4199b8ddec4e89c17d5dd8dee0e5f76b0808952c48c8207c889741755a3350
4
- data.tar.gz: 8fa41c7c5bc360fb097ee12d7525e0b1a1df98ca3b51b12b7cc4b2fc57cdd664
3
+ metadata.gz: c99430b340e0a3ac2c311d2e7688289fb9252598ec0bdc752566f41394503161
4
+ data.tar.gz: a258fbcacc3b2a918b807530c64c0efab06adde90e910ade3915c33d297c97c0
5
5
  SHA512:
6
- metadata.gz: 72548825721c132e94a4b24831bc888e0fc8cfc5f818a27d5041da7a5ef5e33dea1538bd0972890cca9ef9d5332573934a47f816531b86e63f94ef6308e06e73
7
- data.tar.gz: a9cc0f7acf41daf93cf6ad3feb368cf8de76c27f73479365b811afe6e5ae1b3d62773f97f099cf25fdcce738dfe2db5032d34116c0cdca70ca95fcc45493395b
6
+ metadata.gz: b62d2d23daeca75c2d03a63d0bd11b3f55a3206e1355f4946d2c22470fbe1b4f273760732484e135565db78085aa4d579b40dabd266ec59ecc8980f61f64e0a6
7
+ data.tar.gz: 8365b6eebe10b49633b4e070ebb491f6d2fea1b94774c2127b000a58130271227415229eb459bf9fc648c16c565b20a70a23a87b2e0e08cc320ea539b3249714
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- site_hook (0.6.7)
4
+ site_hook (0.6.8)
5
5
  activesupport (~> 5.1)
6
6
  git (~> 1.3)
7
7
  haml (~> 5.0)
@@ -2,7 +2,10 @@
2
2
 
3
3
  require 'site_hook/version'
4
4
  require 'site_hook/sender'
5
+ require 'site_hook/gem'
6
+ require 'site_hook/log'
5
7
  require 'site_hook/logger'
8
+ require 'site_hook/spinner'
6
9
  require 'recursive-open-struct'
7
10
  require 'site_hook/cli'
8
11
  require 'sinatra'
@@ -11,35 +14,12 @@ require 'sass'
11
14
  require 'json'
12
15
  require 'sinatra/json'
13
16
  require 'yaml'
17
+
14
18
  module SiteHook
19
+ autoload :Logs, 'site_hook/log'
20
+ autoload :Gem, 'site_hook/gem'
21
+ autoload :Paths, 'site_hook/paths'
15
22
  # rubocop:disable Metrics/ClassLength, Metrics/LineLength, MethodLength, BlockLength
16
- module Gem
17
- # class Info
18
- class Info
19
- def self.name
20
- 'site_hook'
21
- end
22
-
23
- def self.constant_name
24
- 'SiteHook'
25
- end
26
-
27
- def self.author
28
- 'Ken Spencer <me@iotaspencer.me>'
29
- end
30
- end
31
-
32
- # Paths: Paths to gem resources and things
33
- class Paths
34
- def self.config
35
- Pathname(Dir.home).join('.jph', 'config').to_s
36
- end
37
-
38
- def self.logs
39
- Pathname(Dir.home).join('.jph', 'logs')
40
- end
41
- end
42
- end
43
23
  # class SassHandler (inherits from Sinatra::Base)
44
24
  class SassHandler < Sinatra::Base
45
25
  set :views, Pathname(app_file).dirname.join('site_hook', 'static', 'sass').to_s
@@ -57,160 +37,6 @@ module SiteHook
57
37
  end
58
38
  end
59
39
  # class Webhook (inherits from Sinatra::Base)
60
- class Webhook < Sinatra::Base
61
- HOOKLOG = SiteHook::HookLogger::HookLog.new(SiteHook.log_levels['hook']).log
62
- BUILDLOG = SiteHook::HookLogger::BuildLog.new(SiteHook.log_levels['build']).log
63
- APPLOG = SiteHook::HookLogger::AppLog.new(SiteHook.log_levels['app']).log
64
- JPHRC = YAML.load_file(Pathname(Dir.home).join('.jph', 'config'))
65
- set port: JPHRC.fetch('port', 9090)
66
- set bind: '127.0.0.1'
67
- set server: %w[thin]
68
- set quiet: true
69
- set raise_errors: true
70
- set views: Pathname(app_file).dirname.join('site_hook', 'views')
71
- set :public_folder, Pathname(app_file).dirname.join('site_hook', 'static')
72
- use SassHandler
73
- use CoffeeHandler
74
-
75
- #
76
- # @param [String] body JSON String of body
77
- # @param [String] sig Signature or token from git service
78
- # @param [String] secret User-defined verification token
79
- # @param [Boolean] plaintext Whether the verification is plaintext
80
- def self.verified?(body, sig, secret, plaintext:, service:)
81
- if plaintext
82
- sig == secret
83
- else
84
- case service
85
- when 'gogs'
86
- if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)
87
- APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)}"
88
- true
89
- end
90
- when 'github'
91
- if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)
92
- APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)}"
93
- true
94
- end
95
- end
96
-
97
- end
98
- end
99
40
 
100
- get '/' do
101
- halt 403, { 'Content-Type' => 'text/html' }, '<h1>See <a href="/webhooks/">here</a> for the active webhooks</h1>'
102
- end
103
-
104
- get '/webhooks.json', provides: :json do
105
- content_type 'application/json'
106
- public_projects = JPHRC['projects'].select do |_project, hsh|
107
- (hsh.fetch('private', nil) == false) || hsh.fetch('private', nil).nil?
108
- end
109
- result = {}
110
- public_projects.each do |project, hsh|
111
- result[project] = {}
112
- hsh.delete('hookpass')
113
- result[project].merge!(hsh)
114
- end
115
- headers 'Content-Type' => 'application/json', 'Accept' => 'application/json'
116
- json result, layout: false
117
- end
118
-
119
- get '/webhooks/?' do
120
- haml :webhooks, locals: { 'projects' => JPHRC['projects'] }
121
- end
122
-
123
- get '/webhook/*' do
124
- if params[:splat]
125
- pass
126
- else
127
- halt 405, { 'Content-Type' => 'application/json' }, { message: 'GET not allowed' }.to_json
128
- end
129
- end
130
- post '/webhook/:hook_name/?' do
131
- service = nil
132
- request.body.rewind
133
- req_body = request.body.read
134
- js = RecursiveOpenStruct.new(JSON.parse(req_body))
135
-
136
- projects = JPHRC['projects']
137
- begin
138
- project = projects.fetch(params[:hook_name])
139
- rescue KeyError => e
140
- halt 404, { 'Content-Type' => 'application/json' }, { message: 'no such project', status: 1 }.to_json
141
- end
142
- plaintext = false
143
- signature = nil
144
- event = nil
145
- github = request.env.fetch('HTTP_X_GITHUB_EVENT', nil)
146
- unless github.nil?
147
- event = 'push' if github == 'push'
148
- end
149
- gitlab = request.env.fetch('HTTP_X_GITLAB_EVENT', nil)
150
- unless gitlab.nil?
151
- event = 'push' if gitlab == 'push'
152
- end
153
- gogs = request.env.fetch('HTTP_X_GOGS_EVENT', nil)
154
- unless gogs.nil?
155
- event = 'push' if gogs == 'push'
156
- end
157
- events = { 'github' => github, 'gitlab' => gitlab, 'gogs' => gogs }
158
- events_m_e = events.values.one?
159
- case events_m_e
160
- when true
161
- event = 'push'
162
- service = events.select { |_key, value| value }.keys.first
163
- when false
164
- halt 400, { 'Content-Type': 'application/json' }, { message: 'events are mutually exclusive', status: 'failure' }.to_json
165
-
166
- else
167
- halt 400,
168
- { 'Content-Type': 'application/json' },
169
- 'status': 'failure', 'message': 'something weird happened'
170
- end
171
- if event != 'push'
172
- if event.nil?
173
- halt 400, { 'Content-Type': 'application/json' }, { message: 'no event header' }.to_json
174
- end
175
- end
176
- case service
177
- when 'gitlab'
178
- signature = request.env.fetch('HTTP_X_GITLAB_TOKEN', '')
179
- plaintext = true
180
- when 'github'
181
- signature = request.env.fetch('HTTP_X_HUB_SIGNATURE', '').sub!(/^sha1=/, '')
182
- plaintext = false
183
-
184
- when 'gogs'
185
- signature = request.env.fetch('HTTP_X_GOGS_SIGNATURE', '')
186
- plaintext = false
187
- end
188
- if Webhook.verified?(req_body.to_s, signature, project['hookpass'], plaintext: plaintext, service: service)
189
- BUILDLOG.info 'Building...'
190
-
191
- jekyllbuild = SiteHook::Senders::Jekyll.build(project['src'], project['dst'], BUILDLOG)
192
- jekyll_status = jekyllbuild.fetch(:status, 1)
193
- case jekyll_status
194
-
195
- when 0
196
- status 200
197
- headers 'Content-Type' => 'application/json'
198
- body { { 'status': 'success' }.to_json }
199
- when -1, -2, -3
200
- status 400
201
- headers 'Content-Type' => 'application/json'
202
- body do
203
- { 'status': 'exception', error: jekyll_status.fetch(:message).to_s }
204
- end
205
- end
206
-
207
- else
208
- halt 403, { 'Content-Type' => 'application/json' }, { message: 'incorrect secret', 'status': 'failure' }.to_json
209
- end
210
- end
211
- post '/webhook/?' do
212
- halt 403, { 'Content-Type' => 'application/json' }, { message: 'pick a hook', error: 'root webhook hit', 'status': 'failure' }.to_json
213
- end
214
- end
215
41
  # rubocop:enable Metrics/ClassLength, Metrics/LineLength, MethodLength, BlockLength
216
42
  end
@@ -1,26 +1,7 @@
1
1
  require 'thor'
2
-
3
2
  require 'site_hook/config_class'
3
+ require 'site_hook/server_class'
4
4
  module SiteHook
5
- def self.log_levels
6
- default = {
7
- 'hook' => 'info',
8
- 'build' => 'info',
9
- 'git' => 'info',
10
- 'app' => 'info'
11
- }
12
- begin
13
- log_level = YAML.load_file(Pathname(Dir.home).join('.jph-rc')).fetch('log_levels')
14
- if log_level
15
- log_level
16
- end
17
- rescue KeyError
18
- default
19
- rescue Errno::ENOENT
20
- default
21
- end
22
- end
23
-
24
5
  class CLI < Thor
25
6
  map %w[--version -v] => :__print_version
26
7
  desc '--version, -v', 'Print the version'
@@ -39,15 +20,11 @@ module SiteHook
39
20
  say "Gem Author: #{SiteHook::Gem::Info.author}"
40
21
  say "Gem Version: v#{SiteHook::VERSION}"
41
22
  end
42
-
43
- method_option(:log_levels, type: :hash, banner: 'LEVELS', default: SiteHook.log_levels)
44
- desc 'start', 'Start SiteHook'
45
- def start
46
-
47
- SiteHook.mklogdir unless SiteHook::Gem::Paths.logs.exist?
48
- SiteHook::Webhook.run!
49
- end
50
23
  desc 'config SUBCOMMAND [OPTIONS]', 'Configure site_hook options'
51
24
  subcommand('config', SiteHook::ConfigClass)
25
+ desc 'server SUBCOMMAND [OPTIONS]', 'Start the server'
26
+ subcommand('server', SiteHook::ServerClass)
27
+
28
+
52
29
  end
53
30
  end
@@ -33,7 +33,7 @@ module SiteHook
33
33
  ' private: true/false # hidden from the public list',
34
34
  ''
35
35
  ]
36
- jphrc = SiteHook::Gem::Paths.config
36
+ jphrc = SiteHook::Paths.config
37
37
  if jphrc.exist?
38
38
  puts "#{jphrc} exists. Will not overwrite."
39
39
  else
@@ -0,0 +1,26 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/gem.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:20:45
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+ module SiteHook
10
+ module Gem
11
+ # class Info
12
+ class Info
13
+ def self.name
14
+ 'site_hook'
15
+ end
16
+
17
+ def self.constant_name
18
+ 'SiteHook'
19
+ end
20
+
21
+ def self.author
22
+ 'Ken Spencer <me@iotaspencer.me>'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module SiteHook
2
+ autoload :Paths, 'site_hook/paths'
3
+ # Logs
4
+ # Give logs related methods
5
+ module Logs
6
+ module_function
7
+ def self.log_levels
8
+ default = {
9
+ 'hook' => 'info',
10
+ 'build' => 'info',
11
+ 'git' => 'info',
12
+ 'app' => 'info'
13
+ }
14
+ begin
15
+ log_level = YAML.load_file(SiteHook::Paths.config).fetch('log_levels')
16
+ if log_level
17
+ log_level
18
+ end
19
+ rescue KeyError
20
+ default
21
+ rescue Errno::ENOENT
22
+ default
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/paths.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:23:00
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+
10
+ module SiteHook
11
+ # Paths: Paths to gem resources and things
12
+ class Paths
13
+ def self.config
14
+ Pathname(Dir.home).join('.jph', 'config')
15
+ end
16
+
17
+ def self.logs
18
+ Pathname(Dir.home).join('.jph', 'logs')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/config_class.1.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:45:36
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+ require 'thor'
10
+ module SiteHook
11
+ autoload :Webhook, 'site_hook/webhook'
12
+
13
+ # *ServerClass*
14
+ #
15
+ # Holds all of the commands for the config subcommand
16
+ class ServerClass < Thor
17
+ method_option(:log_levels, type: :hash, banner: 'LEVELS', default: SiteHook::Logs.log_levels)
18
+ desc 'listen', 'Start SiteHook'
19
+ def listen
20
+ SiteHook.mklogdir unless SiteHook::Paths.logs.exist?
21
+ SiteHook::Webhook.run!
22
+ end
23
+ end
24
+ end
25
+ # rubocop:enable Metrics/AbcSize
@@ -1,3 +1,3 @@
1
1
  module SiteHook
2
- VERSION = "0.6.8"
2
+ VERSION = "0.6.9"
3
3
  end
@@ -0,0 +1,169 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/webhook.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:35:44
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+
10
+ require 'sinatra'
11
+
12
+ module SiteHook
13
+ class Webhook < Sinatra::Base
14
+ HOOKLOG = SiteHook::HookLogger::HookLog.new(SiteHook::Logs.log_levels['hook']).log
15
+ BUILDLOG = SiteHook::HookLogger::BuildLog.new(SiteHook::Logs.log_levels['build']).log
16
+ APPLOG = SiteHook::HookLogger::AppLog.new(SiteHook::Logs.log_levels['app']).log
17
+ JPHRC = YAML.load_file(Pathname(Dir.home).join('.jph', 'config'))
18
+
19
+ set port: JPHRC.fetch('port', 9090)
20
+ set bind: '127.0.0.1'
21
+ set server: %w[thin]
22
+ set quiet: true
23
+ set raise_errors: true
24
+ set views: Pathname(app_file).dirname.join('site_hook', 'views')
25
+ set :public_folder, Pathname(app_file).dirname.join('site_hook', 'static')
26
+ use SassHandler
27
+ use CoffeeHandler
28
+
29
+ #
30
+ # @param [String] body JSON String of body
31
+ # @param [String] sig Signature or token from git service
32
+ # @param [String] secret User-defined verification token
33
+ # @param [Boolean] plaintext Whether the verification is plaintext
34
+ def self.verified?(body, sig, secret, plaintext:, service:)
35
+ if plaintext
36
+ sig == secret
37
+ else
38
+ case service
39
+ when 'gogs'
40
+ if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)
41
+ APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)}"
42
+ true
43
+ end
44
+ when 'github'
45
+ if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)
46
+ APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)}"
47
+ true
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+
54
+ get '/' do
55
+ halt 403, { 'Content-Type' => 'text/html' }, '<h1>See <a href="/webhooks/">here</a> for the active webhooks</h1>'
56
+ end
57
+
58
+ get '/webhooks.json', provides: :json do
59
+ content_type 'application/json'
60
+ public_projects = JPHRC['projects'].select do |_project, hsh|
61
+ (hsh.fetch('private', nil) == false) || hsh.fetch('private', nil).nil?
62
+ end
63
+ result = {}
64
+ public_projects.each do |project, hsh|
65
+ result[project] = {}
66
+ hsh.delete('hookpass')
67
+ result[project].merge!(hsh)
68
+ end
69
+ headers 'Content-Type' => 'application/json', 'Accept' => 'application/json'
70
+ json result, layout: false
71
+ end
72
+
73
+ get '/webhooks/?' do
74
+ haml :webhooks, locals: { 'projects' => JPHRC['projects'] }
75
+ end
76
+
77
+ get '/webhook/*' do
78
+ if params[:splat]
79
+ pass
80
+ else
81
+ halt 405, { 'Content-Type' => 'application/json' }, { message: 'GET not allowed' }.to_json
82
+ end
83
+ end
84
+ post '/webhook/:hook_name/?' do
85
+ service = nil
86
+ request.body.rewind
87
+ req_body = request.body.read
88
+ js = RecursiveOpenStruct.new(JSON.parse(req_body))
89
+
90
+ projects = JPHRC['projects']
91
+ begin
92
+ project = projects.fetch(params[:hook_name])
93
+ rescue KeyError => e
94
+ halt 404, { 'Content-Type' => 'application/json' }, { message: 'no such project', status: 1 }.to_json
95
+ end
96
+ plaintext = false
97
+ signature = nil
98
+ event = nil
99
+ github = request.env.fetch('HTTP_X_GITHUB_EVENT', nil)
100
+ unless github.nil?
101
+ event = 'push' if github == 'push'
102
+ end
103
+ gitlab = request.env.fetch('HTTP_X_GITLAB_EVENT', nil)
104
+ unless gitlab.nil?
105
+ event = 'push' if gitlab == 'push'
106
+ end
107
+ gogs = request.env.fetch('HTTP_X_GOGS_EVENT', nil)
108
+ unless gogs.nil?
109
+ event = 'push' if gogs == 'push'
110
+ end
111
+ events = { 'github' => github, 'gitlab' => gitlab, 'gogs' => gogs }
112
+ events_m_e = events.values.one?
113
+ case events_m_e
114
+ when true
115
+ event = 'push'
116
+ service = events.select { |_key, value| value }.keys.first
117
+ when false
118
+ halt 400, { 'Content-Type': 'application/json' }, { message: 'events are mutually exclusive', status: 'failure' }.to_json
119
+
120
+ else
121
+ halt 400,
122
+ { 'Content-Type': 'application/json' },
123
+ 'status': 'failure', 'message': 'something weird happened'
124
+ end
125
+ if event != 'push'
126
+ if event.nil?
127
+ halt 400, { 'Content-Type': 'application/json' }, { message: 'no event header' }.to_json
128
+ end
129
+ end
130
+ case service
131
+ when 'gitlab'
132
+ signature = request.env.fetch('HTTP_X_GITLAB_TOKEN', '')
133
+ plaintext = true
134
+ when 'github'
135
+ signature = request.env.fetch('HTTP_X_HUB_SIGNATURE', '').sub!(/^sha1=/, '')
136
+ plaintext = false
137
+
138
+ when 'gogs'
139
+ signature = request.env.fetch('HTTP_X_GOGS_SIGNATURE', '')
140
+ plaintext = false
141
+ end
142
+ if Webhook.verified?(req_body.to_s, signature, project['hookpass'], plaintext: plaintext, service: service)
143
+ BUILDLOG.info 'Building...'
144
+
145
+ jekyllbuild = SiteHook::Senders::Jekyll.build(project['src'], project['dst'], BUILDLOG)
146
+ jekyll_status = jekyllbuild.fetch(:status, 1)
147
+ case jekyll_status
148
+
149
+ when 0
150
+ status 200
151
+ headers 'Content-Type' => 'application/json'
152
+ body { { 'status': 'success' }.to_json }
153
+ when -1, -2, -3
154
+ status 400
155
+ headers 'Content-Type' => 'application/json'
156
+ body do
157
+ { 'status': 'exception', error: jekyll_status.fetch(:message).to_s }
158
+ end
159
+ end
160
+
161
+ else
162
+ halt 403, { 'Content-Type' => 'application/json' }, { message: 'incorrect secret', 'status': 'failure' }.to_json
163
+ end
164
+ end
165
+ post '/webhook/?' do
166
+ halt 403, { 'Content-Type' => 'application/json' }, { message: 'pick a hook', error: 'root webhook hit', 'status': 'failure' }.to_json
167
+ end
168
+ end
169
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: site_hook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.8
4
+ version: 0.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken Spencer
@@ -290,13 +290,18 @@ files:
290
290
  - lib/site_hook.rb
291
291
  - lib/site_hook/cli.rb
292
292
  - lib/site_hook/config_class.rb
293
+ - lib/site_hook/gem.rb
294
+ - lib/site_hook/log.rb
293
295
  - lib/site_hook/logger.rb
296
+ - lib/site_hook/paths.rb
294
297
  - lib/site_hook/sender.rb
298
+ - lib/site_hook/server_class.rb
295
299
  - lib/site_hook/spinner.rb
296
300
  - lib/site_hook/static/sass/styles.scss
297
301
  - lib/site_hook/version.rb
298
302
  - lib/site_hook/views/layout.haml
299
303
  - lib/site_hook/views/webhooks.haml
304
+ - lib/site_hook/webhook.rb
300
305
  - site_hook.gemspec
301
306
  homepage: https://iotaspencer.me/projects/site_hook/
302
307
  licenses: