forked from hswaw/hscloud
*: Kill frab, smsgw, toot, covid-formity, voucherchecker
Change-Id: I763c758994008db38b47a7e61d3f1b503685aba6 Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1750 Reviewed-by: q3k <q3k@hackerspace.pl>
This commit is contained in:
parent
633fb2e8ce
commit
caf65fcaaf
22 changed files with 12 additions and 1518 deletions
11
CEMETERY.md
Normal file
11
CEMETERY.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# hscloud cemetery
|
||||
|
||||
Here's a list of projects and services that are no longer with us (and the last commit that contained them):
|
||||
|
||||
- covid-formity - 633fb2
|
||||
- toot - 633fb2
|
||||
- frab - 633fb2
|
||||
- smsgw - 633fb2
|
||||
- voucherchecker - 633fb2
|
||||
|
||||
RIP.
|
|
@ -16,7 +16,7 @@ Directory Structure
|
|||
|
||||
Directories you should care about:
|
||||
|
||||
- **app**: external services that we host that are somewhat universal: matrix, covid-formity, etc.
|
||||
- **app**: external services that we host that are somewhat universal: matrix, mastodon, etc.
|
||||
- **bgpwtf**: code related to our little ISP
|
||||
- **cluster**: code related to our Kubernetes cluster (`k0.hswaw.net`)
|
||||
- **dc**: code related to datacenter automation
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
# covid19.hackerspace.pl, a covid-formity instance.
|
||||
# This needs a secret provisioned, create with:
|
||||
# kubectl -n covid-formity create secret generic covid-formity --from-literal=postgres_password=$(pwgen 24 1) --from-literal=secret_key=$(pwgen 24 1) --from-literal=oauth2_secret=...
|
||||
|
||||
local kube = import "../../kube/hscloud.libsonnet";
|
||||
local redis = import "../../kube/redis.libsonnet";
|
||||
local postgres = import "../../kube/postgres.libsonnet";
|
||||
|
||||
{
|
||||
local app = self,
|
||||
local cfg = app.cfg,
|
||||
cfg:: {
|
||||
namespace: "covid-formity",
|
||||
image: "registry.k0.hswaw.net/informatic/covid-formity@sha256:53c5fb0dbc4a6660ab47e39869a516f1e3f833dee5a03867386771bd9ffaf7b8",
|
||||
domain: "covid19.hackerspace.pl",
|
||||
altDomains: ["covid.hackerspace.pl", "www.covid.hackerspace.pl"],
|
||||
},
|
||||
|
||||
metadata(component):: {
|
||||
namespace: app.cfg.namespace,
|
||||
labels: {
|
||||
"app.kubernetes.io/name": "covid-formity",
|
||||
"app.kubernetes.io/managed-by": "kubecfg",
|
||||
"app.kubernetes.io/component": component,
|
||||
},
|
||||
},
|
||||
|
||||
namespace: kube.Namespace(app.cfg.namespace),
|
||||
|
||||
postgres: postgres {
|
||||
cfg+: {
|
||||
namespace: cfg.namespace,
|
||||
appName: "covid-formity",
|
||||
database: "covid-formity",
|
||||
username: "covid-formity",
|
||||
password: { secretKeyRef: { name: "covid-formity", key: "postgres_password" } },
|
||||
},
|
||||
},
|
||||
|
||||
redis: redis {
|
||||
cfg+: {
|
||||
namespace: cfg.namespace,
|
||||
appName: "covid-formity",
|
||||
password: { secretKeyRef: { name: "covid-formity", key: "redis_password" } },
|
||||
storageClassName: app.postgres.cfg.storageClassName,
|
||||
},
|
||||
},
|
||||
|
||||
deployment: kube.Deployment("covid-formity") {
|
||||
metadata+: app.metadata("covid-formity"),
|
||||
spec+: {
|
||||
replicas: 1,
|
||||
template+: {
|
||||
spec+: {
|
||||
containers_: {
|
||||
web: kube.Container("covid-formity") {
|
||||
image: cfg.image,
|
||||
ports_: {
|
||||
http: { containerPort: 5000 },
|
||||
},
|
||||
env_: {
|
||||
DATABASE_HOSTNAME: "postgres",
|
||||
DATABASE_USERNAME: app.postgres.cfg.username,
|
||||
DATABASE_PASSWORD: app.postgres.cfg.password,
|
||||
CACHE_REDIS_PASSWORD: app.redis.cfg.password,
|
||||
CACHE_REDIS_URL: "redis://default:$(CACHE_REDIS_PASSWORD)@redis",
|
||||
DATABASE_NAME: app.postgres.cfg.appName,
|
||||
SPACEAUTH_CONSUMER_KEY: "covid-formity",
|
||||
SPACEAUTH_CONSUMER_SECRET: { secretKeyRef: { name: "covid-formity", key: "oauth2_secret" } },
|
||||
SECRET_KEY: { secretKeyRef: { name: "covid-formity", key: "secret_key" } },
|
||||
SHIPPING_KURJERZY_EMAIL: "qrde@hackerspace.pl",
|
||||
SHIPPING_KURJERZY_PASSWORD: { secretKeyRef: { name: "covid-formity-shipping", key: "kurjerzy_password" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
svc: kube.Service("covid-formity") {
|
||||
metadata+: app.metadata("covid-formity"),
|
||||
target_pod:: app.deployment.spec.template,
|
||||
spec+: {
|
||||
ports: [
|
||||
{ name: "http", port: 5000, targetPort: 5000, protocol: "TCP" },
|
||||
],
|
||||
type: "ClusterIP",
|
||||
},
|
||||
},
|
||||
|
||||
ingress: kube.SimpleIngress("covid-formity") {
|
||||
hosts:: [cfg.domain] + cfg.altDomains,
|
||||
target_service:: app.svc,
|
||||
metadata+: app.metadata("covid-formity") {
|
||||
annotations+: {
|
||||
"nginx.ingress.kubernetes.io/configuration-snippet": "
|
||||
location /qr1 { rewrite ^/qr1(.*)$ https://covid.hackerspace.pl$1 redirect; }
|
||||
location /video { return 302 https://youtu.be/eC19w2NFO0E; }
|
||||
location /manual { return 302 https://wiki.hackerspace.pl/_media/projects:covid-19:przylbica-instrukcja-v1.0.pdf; }
|
||||
",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
# toot.hackerspace.pl, a Mastodon instance.
|
||||
# This needs a secret provisioned, create with:
|
||||
# kubectl -n toot create secret generic mastodon --from-literal=postgres_password=$(pwgen 24 1)
|
||||
|
||||
local kube = import "../../kube/kube.libsonnet";
|
||||
local postgres = import "../../kube/postgres.libsonnet";
|
||||
local redis = import "../../kube/redis.libsonnet";
|
||||
|
||||
{
|
||||
local app = self,
|
||||
local cfg = app.cfg,
|
||||
cfg:: {
|
||||
namespace: "toot",
|
||||
},
|
||||
|
||||
metadata(component):: {
|
||||
namespace: app.cfg.namespace,
|
||||
labels: {
|
||||
"app.kubernetes.io/name": "toot",
|
||||
"app.kubernetes.io/managed-by": "kubecfg",
|
||||
"app.kubernetes.io/component": component,
|
||||
},
|
||||
},
|
||||
|
||||
namespace: kube.Namespace(app.cfg.namespace),
|
||||
|
||||
postgres: postgres {
|
||||
cfg+: {
|
||||
namespace: cfg.namespace,
|
||||
appName: "toot",
|
||||
database: "mastodon",
|
||||
username: "mastodon",
|
||||
password: { secretKeyRef: { name: "mastodon", key: "postgres_password" } },
|
||||
},
|
||||
},
|
||||
|
||||
redis: redis {
|
||||
cfg+: {
|
||||
namespace: cfg.namespace,
|
||||
appName: "toot",
|
||||
},
|
||||
},
|
||||
}
|
|
@ -327,9 +327,6 @@ local admins = import "lib/admins.libsonnet";
|
|||
// hijacked by other cluster users, you should also state
|
||||
// it here (either as a wildcard, or unary domains).
|
||||
allow_domain: [
|
||||
{ namespace: "covid-formity", dns: "covid19.hackerspace.pl" },
|
||||
{ namespace: "covid-formity", dns: "covid.hackerspace.pl" },
|
||||
{ namespace: "covid-formity", dns: "www.covid.hackerspace.pl" },
|
||||
{ namespace: "inventory", dns: "inventory.hackerspace.pl" },
|
||||
{ namespace: "capacifier", dns: "capacifier.hackerspace.pl" },
|
||||
{ namespace: "ldapweb", dns: "profile.hackerspace.pl" },
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
local mirko = import "../../kube/mirko.libsonnet";
|
||||
local kube = import "../../kube/kube.libsonnet";
|
||||
local postgres = import "../../kube/postgres.libsonnet";
|
||||
|
||||
{
|
||||
local cfg = self.cfg,
|
||||
cfg:: {
|
||||
image: "frab/frab@sha256:30051f5153c4f02a8a1bee4b306bd696e2b018f2b13d16bd9c681fc1d633de3e",
|
||||
storageClassName: error "storageClassName must be set!",
|
||||
webFQDN: error "webFQDN must be set!",
|
||||
|
||||
secret: {
|
||||
secretKeyBase: error "secretKeyBase must be set!",
|
||||
smtpPassword: error "smtpPassword must be set!",
|
||||
},
|
||||
|
||||
smtp: {
|
||||
server: "mail.hackerspace.pl",
|
||||
from: "frab@hackerspace.pl",
|
||||
username: "frab",
|
||||
},
|
||||
},
|
||||
|
||||
component(cfg, env): mirko.Component(env, "frab") {
|
||||
local frab = self,
|
||||
cfg+: {
|
||||
image: cfg.image,
|
||||
volumes+: {
|
||||
public: kube.PersistentVolumeClaimVolume(frab.volumePublic),
|
||||
},
|
||||
|
||||
pgpass:: { secretKeyRef: { name: frab.makeName("-postgres"), key: "postgres_password", } },
|
||||
|
||||
container: frab.Container("main") {
|
||||
volumeMounts_+: {
|
||||
public: { mountPath: "/home/frab/app/public", },
|
||||
},
|
||||
// order matters (for POSTGRES_PASS substitution), we don't use env_
|
||||
env: [
|
||||
{ name: "TZ", value: "Europe/Warsaw" },
|
||||
{ name: "POSTGRES_PASS", valueFrom: frab.cfg.pgpass },
|
||||
{ name: "DATABASE_URL", value: "postgresql://frab:$(POSTGRES_PASS)@%s/frab" % [frab.postgres.svc.host] },
|
||||
{ name: "SECRET_KEY_BASE", valueFrom: kube.SecretKeyRef(frab.secret, "secretKeyBase") },
|
||||
{ name: "FROM_EMAIL", value: cfg.smtp.from },
|
||||
{ name: "SMTP_ADDRESS", value: cfg.smtp.server },
|
||||
{ name: "SMTP_USERNAME", value: cfg.smtp.username },
|
||||
{ name: "SMTP_PASSWORD", valueFrom: kube.SecretKeyRef(frab.secret, "smtpPassword") },
|
||||
{ name: "SMTP_PORT", value: "587" },
|
||||
{ name: "SMTP_NOTLS", value: "false" },
|
||||
],
|
||||
resources: {
|
||||
// thicc RoR
|
||||
requests: {
|
||||
cpu: "100m",
|
||||
memory: "512Mi",
|
||||
},
|
||||
limits: {
|
||||
cpu: "1",
|
||||
memory: "1Gi",
|
||||
},
|
||||
},
|
||||
},
|
||||
ports+: {
|
||||
publicHTTP: {
|
||||
web: {
|
||||
port: 3000,
|
||||
dns: cfg.webFQDN,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
secret: kube.Secret(frab.makeName("-secret")) {
|
||||
metadata+: frab.metadata,
|
||||
data: cfg.secret,
|
||||
},
|
||||
|
||||
postgres: postgres {
|
||||
cfg+: {
|
||||
namespace: frab.metadata.namespace,
|
||||
appName: "frab",
|
||||
storageClassName: cfg.storageClassName,
|
||||
prefix: frab.makeName("-postgres") + "-",
|
||||
database: "frab",
|
||||
username: "frab",
|
||||
password: frab.cfg.pgpass,
|
||||
},
|
||||
},
|
||||
|
||||
volumePublic: kube.PersistentVolumeClaim(frab.makeName("-public")) {
|
||||
metadata+: frab.metadata,
|
||||
spec+: {
|
||||
storageClassName: cfg.storageClassName,
|
||||
accessModes: ["ReadWriteOnce"],
|
||||
resources: {
|
||||
requests: {
|
||||
storage: "5Gi",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
local mirko = import "../../kube/mirko.libsonnet";
|
||||
local kube = import "../../kube/kube.libsonnet";
|
||||
|
||||
local smsgw = import "smsgw.libsonnet";
|
||||
local teleimg = import "teleimg.libsonnet";
|
||||
local frab = import "frab.libsonnet";
|
||||
local pretalx = import "pretalx.libsonnet";
|
||||
local cebulacamp = import "cebulacamp.libsonnet";
|
||||
|
||||
|
@ -13,18 +11,14 @@ local cebulacamp = import "cebulacamp.libsonnet";
|
|||
local cfg = self.cfg,
|
||||
|
||||
cfg+: {
|
||||
smsgw: smsgw.cfg,
|
||||
teleimg: teleimg.cfg,
|
||||
frab: frab.cfg,
|
||||
pretalx: pretalx.cfg,
|
||||
cebulacamp: cebulacamp.cfg,
|
||||
},
|
||||
|
||||
components: {
|
||||
smsgw: smsgw.component(cfg.smsgw, env),
|
||||
teleimg: teleimg.teleimg(cfg.teleimg, env),
|
||||
lelegram: teleimg.lelegram(cfg.teleimg, env),
|
||||
frab: frab.component(cfg.frab, env),
|
||||
pretalx: pretalx.component(cfg.pretalx, env) {
|
||||
cronjob: null,
|
||||
},
|
||||
|
@ -34,26 +28,12 @@ local cebulacamp = import "cebulacamp.libsonnet";
|
|||
|
||||
prod: self.hswaw("hswaw-prod") {
|
||||
cfg+: {
|
||||
smsgw+: {
|
||||
secret+: {
|
||||
twilio_token: std.base64(std.split(importstr "secrets/plain/prod-twilio-token", "\n")[0]),
|
||||
},
|
||||
webhookFQDN: "smsgw-webhook-prod.hswaw.net",
|
||||
},
|
||||
teleimg+: {
|
||||
webFQDN: "teleimg.hswaw.net",
|
||||
secret+: {
|
||||
telegram_token: std.base64(std.split(importstr "secrets/plain/prod-telegram-token", "\n")[0]),
|
||||
},
|
||||
},
|
||||
frab+: {
|
||||
storageClassName: "waw-hdd-redundant-3",
|
||||
webFQDN: "frab.hackerspace.pl",
|
||||
secret+: {
|
||||
secretKeyBase: std.base64(std.split(importstr "secrets/plain/prod-frab-smtp-password", "\n")[0]),
|
||||
smtpPassword: std.base64(std.split(importstr "secrets/plain/prod-frab-secret-key-base", "\n")[0]),
|
||||
},
|
||||
},
|
||||
pretalx+: {
|
||||
storageClassName: "waw-hdd-redundant-3",
|
||||
webFQDN: "cfp.cebula.camp",
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
=SBGB
|
||||
-----END PGP MESSAGE-----
|
|
@ -1,40 +0,0 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQEMAzhuiT4RC8VbAQf/Yw5z7cIXkbhOo1l57I1UQYtYaV3Z2iPmg6vf2zZIf+Pa
|
||||
XlRQtExMGt3tjRD5oItQ3e3fYtHvH1HEmzeFSATcoKcx/CHq803zsuV6vS8Z8uvn
|
||||
ISC1dW1cx/njWbj4suEyL/vQlyWC71L/xd1b/rj0eoJJCIk9J/e+EaTQ0OXiKaVJ
|
||||
Og2EhrTTtqvt6mm4cRrnAcw3+YBbsqRCQYuSx6reeHE93fy/zCr7vDLmySrOwmvl
|
||||
hNsu18JSn7m+tybIGsmAZ2K7Ayfvjk0BHTerkw9zlc1vJFkd2DG8rU/Tx924t7ND
|
||||
v3738BS7wX5MmzmSK9gAYBuf/EVU9PTUtCEibcUd34UBDANcG2tp6fXqvgEIAJ/y
|
||||
/KWEwPMJKFf6b8ipMCW4BapV40g2SPg4gXNEWwZfd5UH5bBcZMkF0G3NUtbN6P/L
|
||||
rrkeRwPpJO1PYeGN/6xysf4xKWFBB2YYYVU7LVBSBj78q+ZDgkqUVCE5u5qPXD0C
|
||||
I/28KNtY40d82aY9zSVdU30snecyJ7VZcmms0kmgBvvctEzMCcHOkjwcA6w9JhzR
|
||||
xMoN4QeZt+tG9eErp7ZJk1TLR4e156fxSXneOBsRtjyGtUSB7TQjDLHO+pXiiWGN
|
||||
9N2KBJudhCzIoV89WV5kPksoqFOtT4KwQOPRnqHI21YNDj16TUbvxnYVTdI/43Ux
|
||||
MhT4YF4d+1tjP1HxUWGFAgwDodoT8VqRl4UBD/9kUQmIH54Vg8Hy8OIyRZcZMKOc
|
||||
1m8nHOQKFB9JlxrpgMSEnsI+f8uBFoO2jERVErBRcvJ+wZVkCw8D+4x35HD1toyo
|
||||
AykEtcqx3HaaKawW00RMm3IR/HW2ylcmGkkURU2CIkKwuqo1Ycdz81SSF05NQNOD
|
||||
bLpzLxQPiuIR7e/I6aK6j0u0gOu5/cFzFie1+LxNZOcBvmzOpphsT0Xqj8uiow3V
|
||||
p9NMrCI6Rm8Pkw+duvnvRPXi2dPJelzUxVbJMMe4PNXEuZoKsSTf6LqcAvMo0rCE
|
||||
R73cX/xhlTwxDCEVWiUGHsx0j1EC8t8qz4fyoZibVLxjcqyWsa9F7bf77YkmBtyb
|
||||
EFVjAqkbvCskd3z8JwYxd9KU++RtHhCDl0eCU/o5K8A9ZtdJkO0v8kTU+jcCkA3f
|
||||
HJvOieb/zRTuVrCcDRtXJI9DOfEbwmfZXi7WzQJkIQADQLbLz9OiWZ15WTxOlrHw
|
||||
xDV1N3uzfZL4GPMdkeGZancDoAQyBp94bVKzAmyC8dGFUQ74qxdQuzlMbudxmOYP
|
||||
s8//wyoK1muzoj0x9XsVYuHiRhHfuqIEYvsbDm3ITq6fuINirsRq6SeruFlyp9NP
|
||||
GmTRAQRPjV7GBw/Wwx3eXcnTXhHZgo0+QKkvac89ITt0mje3nSlGNbRMEJvYaBi4
|
||||
LktwnIvbFSyY7XHct4UCDAPiA8lOXOuz7wEP/ikorgFPBVC+CVdanq9EwS06XA28
|
||||
bP8avXBp5FPRzyU6OeDr03sC9z0PfS7K9JkLxRuuclNYqbB5I8v+i+Vs+Wj2SIly
|
||||
CtlCQxQTkgxVSmlqCTtmpp1y+Kyo0rA7SKf3De5eCFOHfbedyiddPizVjNYyN7ZR
|
||||
p9X/Z5czA3AzPc3/cf1H5RBDWZGPfxKuwuMpjMcj8h7encDlOLcROHcUdmJJ7LB4
|
||||
FNfSnltKdApQbcqgcGZsyy7RYEqkyU7GEfZsOTgkNw5oRr+CNhHN8XSPmwSRaVuG
|
||||
iUCb6yK2HjhhhK6Xk+EK26pmTB47I+P1Q7CDn38t83AxwB+JarvfzLSZbOwW+ZsJ
|
||||
Vc5PtwMKF/x+svq0MW/Ub7Lr3J0vf9PkYnTcm5kMlkCDiEaAudjRaUC3++1oV0M7
|
||||
5KLGacdqgkBJ+6WrSm58GiKs25kNTjwKvSaSOJ61+oO2vmJ5GWFB9PtKJlGAZlmS
|
||||
/AWBfXr7LMwa9jJHwWM21o4LjpOkc5ADwPVf3Vp+gRQlcg42xpa8GYOb8ZDwbNiw
|
||||
oYCIxSqk3xFlcsqf6xRFxNwHsv4/yTdAw4xkk116a0KCsdMUj77YlNuck4aZ0+P8
|
||||
AVs30qnRCiDEoHZKeu3R6yi9YmKV5fRxpQ7xHXqX2RuVn9CMUSFwbKR5PxqJpi67
|
||||
fAFi/sjlPrCBaxle0nkBUrchhcWs2wi2r+ZFFBdw0TYsVpo1zG368asw0i9f+qfi
|
||||
+mpNstI5nXbQrZ4tzduAhMqxVmkiT+JrABqFDLf0M/+Ej/z3I/Gr12ffMq7Fo4Q8
|
||||
0rfiwlWXhadv7+KqCXcf8AGSVlZEM/s9uF8B6JquIDt2B8nS9fzR
|
||||
=hseZ
|
||||
-----END PGP MESSAGE-----
|
|
@ -1,40 +0,0 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
=bRrv
|
||||
-----END PGP MESSAGE-----
|
|
@ -1,40 +0,0 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
WtNaMpCfU92nkD2ZwTp96oiLUOVbWEaGGByh
|
||||
=IR9z
|
||||
-----END PGP MESSAGE-----
|
|
@ -1,81 +0,0 @@
|
|||
local mirko = import "../../kube/mirko.libsonnet";
|
||||
local kube = import "../../kube/kube.libsonnet";
|
||||
|
||||
{
|
||||
cfg:: {
|
||||
secret: {
|
||||
twilio_token: error "twilio_token must be set",
|
||||
},
|
||||
image: "registry.k0.hswaw.net/q3k/smsgs:1570049853-05c5b491c45de6d960979d4aee8635768f3178e9",
|
||||
webhookFQDN: error "webhookFQDN must be set",
|
||||
},
|
||||
|
||||
component(cfg, env):: mirko.Component(env, "smsgw") {
|
||||
local smsgw = self,
|
||||
cfg+: {
|
||||
image: cfg.image,
|
||||
container: smsgw.GoContainer("main", "/smsgw/smsgw") {
|
||||
env_: {
|
||||
TWILIO_TOKEN: kube.SecretKeyRef(smsgw.secret, "twilio_token"),
|
||||
},
|
||||
command+: [
|
||||
"-twilio_friendly_phone", "48732168371",
|
||||
"-twilio_sid", "AC806ed4bf4b6c80c8f8ea686379b69518",
|
||||
"-twilio_token", "$(TWILIO_TOKEN)",
|
||||
"-webhook_listen", "0.0.0.0:5000",
|
||||
"-webhook_public", "https://%s/" % [ cfg.webhookFQDN ],
|
||||
],
|
||||
},
|
||||
ports+: {
|
||||
publicHTTP: {
|
||||
webhook: {
|
||||
port: 5000,
|
||||
dns: cfg.webhookFQDN,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
secret: kube.Secret("smsgw") {
|
||||
metadata+: smsgw.metadata,
|
||||
data: cfg.secret,
|
||||
},
|
||||
|
||||
// Temporary machinery to access gRPC from outsite.
|
||||
// In the future, this will be handled by a proxy/API gateway.
|
||||
// For now, we need this running.
|
||||
// TODO(q3k): remove this when we have an API GW or proxy.
|
||||
stopgap: {
|
||||
local stopgap = self,
|
||||
|
||||
rpcLB: kube.Service("smsgw-tcp-rpc") {
|
||||
metadata+: smsgw.metadata,
|
||||
target_pod: smsgw.deployment.spec.template,
|
||||
spec+: {
|
||||
type: "LoadBalancer",
|
||||
ports: [
|
||||
{ name: "grpc-external", port: 443, targetPort: 4200 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
mkClientCert(name, cn):: kube.Certificate(name) {
|
||||
metadata+: smsgw.metadata,
|
||||
spec: {
|
||||
secretName: name,
|
||||
duration: "35040h0m0s", // 4 years
|
||||
issuerRef: {
|
||||
// Contract with cluster/lib/pki.libsonnet.
|
||||
// Copied over.
|
||||
name: "pki-ca",
|
||||
kind: "ClusterIssuer",
|
||||
},
|
||||
commonName: cn,
|
||||
},
|
||||
},
|
||||
|
||||
kasownikCert: stopgap.mkClientCert("smsgw-tcp-rpc-consumer", "kasownik.external.hswaw.net"),
|
||||
piorekfCert: stopgap.mkClientCert("smsgw-tcp-rpc-piorekf", "piorekf.person.hswaw.net"),
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "smsgw_lib",
|
||||
srcs = [
|
||||
"dispatcher.go",
|
||||
"main.go",
|
||||
"twilio.go",
|
||||
],
|
||||
importpath = "code.hackerspace.pl/hscloud/hswaw/smsgw",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//go/mirko",
|
||||
"//hswaw/smsgw/proto",
|
||||
"@com_github_golang_glog//:glog",
|
||||
"@org_golang_google_grpc//codes",
|
||||
"@org_golang_google_grpc//status",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "smsgw",
|
||||
embed = [":smsgw_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "smsgw_test",
|
||||
srcs = ["dispatcher_test.go"],
|
||||
embed = [":smsgw_lib"],
|
||||
)
|
||||
|
||||
container_layer(
|
||||
name = "layer_bin",
|
||||
directory = "/smsgw/",
|
||||
files = [
|
||||
":smsgw",
|
||||
],
|
||||
)
|
||||
|
||||
container_image(
|
||||
name = "runtime",
|
||||
base = "@prodimage-bionic//image",
|
||||
layers = [
|
||||
":layer_bin",
|
||||
],
|
||||
)
|
||||
|
||||
container_push(
|
||||
name = "push",
|
||||
format = "Docker",
|
||||
image = ":runtime",
|
||||
registry = "registry.k0.hswaw.net",
|
||||
repository = "q3k/smsgs",
|
||||
tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
|
||||
)
|
|
@ -1,110 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// dispatcher is responsible for dispatching incoming SMS messages to subscribers
|
||||
// that have chosen to receive them, filtering accordingly.
|
||||
type dispatcher struct {
|
||||
// New SMS messages to be dispatched.
|
||||
incoming chan *sms
|
||||
// New subscribers to send messages to.
|
||||
subscribers chan *subscriber
|
||||
}
|
||||
|
||||
// newDispatcher creates a new dispatcher.
|
||||
func newDispatcher() *dispatcher {
|
||||
return &dispatcher{
|
||||
incoming: make(chan *sms),
|
||||
subscribers: make(chan *subscriber),
|
||||
}
|
||||
}
|
||||
|
||||
// sms received from the upstream provider.
|
||||
type sms struct {
|
||||
from string
|
||||
body string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// subscriber that wants to receive messages with a given body filter.
|
||||
type subscriber struct {
|
||||
// regexp to filter message body by
|
||||
re *regexp.Regexp
|
||||
// channel to which messages will be sent, must be emptied regularly by the
|
||||
// subscriber.
|
||||
data chan *sms
|
||||
// channel that needs to be closed when the subscriber doesn't want to receive
|
||||
// any more messages.
|
||||
cancel chan struct{}
|
||||
}
|
||||
|
||||
func (p *dispatcher) publish(msg *sms) {
|
||||
p.incoming <- msg
|
||||
}
|
||||
|
||||
func (p *dispatcher) subscribe(sub *subscriber) {
|
||||
p.subscribers <- sub
|
||||
}
|
||||
|
||||
func (p *dispatcher) run(ctx context.Context) {
|
||||
// Map of internal IDs to subscribers. Internal IDs are used to remove
|
||||
// canceled subscribers easily.
|
||||
subscriberMap := make(map[int64]*subscriber)
|
||||
// Internal channel that will emit SIDs of subscribers that needs to be
|
||||
// removed.
|
||||
subscriberCancel := make(chan int64)
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
// Should the processor close?
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
// Do we need to remove a given subscriber?
|
||||
case sid := <-subscriberCancel:
|
||||
delete(subscriberMap, sid)
|
||||
|
||||
// Do we have a new subscriber?
|
||||
case sub := <-p.subscribers:
|
||||
// Generate a SID. A UNIX nanosecond timestamp is enough, since
|
||||
// we're not running in parallel.
|
||||
sid := time.Now().UnixNano()
|
||||
glog.V(5).Infof("New subscriber %x, regexp %v", sid, sub.re)
|
||||
|
||||
// Add to subscriber map.
|
||||
subscriberMap[sid] = sub
|
||||
|
||||
// On sub.cancel closed, emit info that we need to delete that
|
||||
// subscriber.
|
||||
go func() {
|
||||
_, _ = <-sub.cancel
|
||||
subscriberCancel <- sid
|
||||
}()
|
||||
|
||||
// Do we have a new message to dispatch?
|
||||
case in := <-p.incoming:
|
||||
for sid, s := range subscriberMap {
|
||||
glog.V(10).Infof("Considering %x", sid)
|
||||
// If this subscriber doesn't care, ignore.
|
||||
if !s.re.MatchString(in.body) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Send, non-blocking, to subscriber. This ensures that we
|
||||
// don't get stuck if a subscriber doesn't drain fast enough.
|
||||
go func(to *subscriber, sid int64) {
|
||||
glog.V(10).Infof("Dispatching to %x, %v", sid, to.data)
|
||||
to.data <- in
|
||||
glog.V(10).Infof("Dispatched to %x", sid)
|
||||
}(s, sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func makeDut() (*dispatcher, context.CancelFunc, context.Context) {
|
||||
dut := newDispatcher()
|
||||
|
||||
ctx := context.Background()
|
||||
ctxC, cancelCtx := context.WithCancel(ctx)
|
||||
go dut.run(ctxC)
|
||||
|
||||
return dut, cancelCtx, ctx
|
||||
}
|
||||
|
||||
func expectReceived(t *testing.T, s *sms, data chan *sms) {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
select {
|
||||
case d := <-data:
|
||||
if d.from != s.from {
|
||||
t.Errorf("Received SMS from %q, wanted %q", d.from, s.from)
|
||||
}
|
||||
if d.body != s.body {
|
||||
t.Errorf("Received SMS body %q, wanted %q", d.body, s.body)
|
||||
}
|
||||
if d.timestamp != s.timestamp {
|
||||
t.Errorf("Received SMS timestamp %v, wanted %v", d.timestamp, s.timestamp)
|
||||
}
|
||||
case <-ticker.C:
|
||||
t.Fatalf("Timed out waiting for message")
|
||||
}
|
||||
}
|
||||
|
||||
func expectEmpty(t *testing.T, data chan *sms) {
|
||||
ticker := time.NewTicker(1 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
select {
|
||||
case <-data:
|
||||
t.Fatalf("Received unwanted message")
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatcher(t *testing.T) {
|
||||
dut, cancelDut, _ := makeDut()
|
||||
defer cancelDut()
|
||||
|
||||
data := make(chan *sms)
|
||||
cancel := make(chan struct{})
|
||||
|
||||
dut.subscribe(&subscriber{
|
||||
re: regexp.MustCompile(".*"),
|
||||
data: data,
|
||||
cancel: cancel,
|
||||
})
|
||||
|
||||
in := &sms{
|
||||
from: "+4821372137",
|
||||
body: "foo",
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
dut.publish(in)
|
||||
|
||||
// Make sure we ge the message.
|
||||
expectReceived(t, in, data)
|
||||
|
||||
// Make sure we don't receive the message again.
|
||||
expectEmpty(t, data)
|
||||
|
||||
// Publish a new message, but this time close our subscriber.
|
||||
close(cancel)
|
||||
// Hack: yield.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
dut.publish(in)
|
||||
expectEmpty(t, data)
|
||||
}
|
||||
|
||||
type testSubscriber struct {
|
||||
re *regexp.Regexp
|
||||
data chan *sms
|
||||
cancel chan struct{}
|
||||
}
|
||||
|
||||
func TestDispatcherFilters(t *testing.T) {
|
||||
dut, cancelDut, _ := makeDut()
|
||||
defer cancelDut()
|
||||
|
||||
subscribers := []*testSubscriber{
|
||||
{re: regexp.MustCompile(".*")},
|
||||
{re: regexp.MustCompile("foo")},
|
||||
{re: regexp.MustCompile("bar")},
|
||||
}
|
||||
|
||||
for _, s := range subscribers {
|
||||
s.data = make(chan *sms)
|
||||
s.cancel = make(chan struct{})
|
||||
dut.subscribe(&subscriber{
|
||||
re: s.re,
|
||||
data: s.data,
|
||||
cancel: s.cancel,
|
||||
})
|
||||
defer func(c chan struct{}) {
|
||||
close(c)
|
||||
}(s.cancel)
|
||||
}
|
||||
|
||||
in := &sms{
|
||||
from: "+4821372137",
|
||||
body: "foo",
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
dut.publish(in)
|
||||
expectReceived(t, in, subscribers[0].data)
|
||||
expectReceived(t, in, subscribers[1].data)
|
||||
expectEmpty(t, subscribers[2].data)
|
||||
|
||||
in = &sms{
|
||||
from: "+4821372137",
|
||||
body: "bar",
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
dut.publish(in)
|
||||
expectReceived(t, in, subscribers[0].data)
|
||||
expectEmpty(t, subscribers[1].data)
|
||||
expectReceived(t, in, subscribers[2].data)
|
||||
|
||||
in = &sms{
|
||||
from: "+4821372137",
|
||||
body: "foobar",
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
dut.publish(in)
|
||||
expectReceived(t, in, subscribers[0].data)
|
||||
expectReceived(t, in, subscribers[1].data)
|
||||
expectReceived(t, in, subscribers[2].data)
|
||||
}
|
||||
|
||||
func TestDispatcherMany(t *testing.T) {
|
||||
dut, cancelDut, _ := makeDut()
|
||||
defer cancelDut()
|
||||
|
||||
subscribers := make([]*testSubscriber, 10000)
|
||||
|
||||
for i, _ := range subscribers {
|
||||
s := &testSubscriber{
|
||||
re: regexp.MustCompile(".*"),
|
||||
data: make(chan *sms),
|
||||
cancel: make(chan struct{}),
|
||||
}
|
||||
subscribers[i] = s
|
||||
dut.subscribe(&subscriber{
|
||||
re: s.re,
|
||||
data: s.data,
|
||||
cancel: s.cancel,
|
||||
})
|
||||
defer func(c chan struct{}) {
|
||||
close(c)
|
||||
}(s.cancel)
|
||||
}
|
||||
|
||||
in := &sms{
|
||||
from: "+4821372137",
|
||||
body: "foo",
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
dut.publish(in)
|
||||
|
||||
for _, s := range subscribers {
|
||||
expectReceived(t, in, s.data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatcherHammer(t *testing.T) {
|
||||
dut, cancelDut, _ := makeDut()
|
||||
defer cancelDut()
|
||||
|
||||
for i := 0; i < 1000000; i += 1 {
|
||||
s := &testSubscriber{
|
||||
re: regexp.MustCompile(".*"),
|
||||
data: make(chan *sms),
|
||||
cancel: make(chan struct{}),
|
||||
}
|
||||
|
||||
dut.subscribe(&subscriber{
|
||||
re: s.re,
|
||||
data: s.data,
|
||||
cancel: s.cancel,
|
||||
})
|
||||
|
||||
in := &sms{
|
||||
from: "+4821372137",
|
||||
body: "foo",
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
dut.publish(in)
|
||||
expectReceived(t, in, s.data)
|
||||
|
||||
close(s.cancel)
|
||||
}
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.hackerspace.pl/hscloud/go/mirko"
|
||||
"github.com/golang/glog"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
pb "code.hackerspace.pl/hscloud/hswaw/smsgw/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
flagTwilioSID string
|
||||
flagTwilioToken string
|
||||
flagTwilioFriendlyPhone string
|
||||
|
||||
flagWebhookListen string
|
||||
flagWebhookPublic string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Set("logtostderr", "true")
|
||||
}
|
||||
|
||||
type server struct {
|
||||
dispatcher *dispatcher
|
||||
}
|
||||
|
||||
func ourPhoneNumber(ctx context.Context, t *twilio, friendly string) (*incomingPhoneNumber, error) {
|
||||
ipn, err := t.getIncomingPhoneNumbers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pn := range ipn {
|
||||
if pn.FriendlyName == friendly {
|
||||
return &pn, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("requested phone number %q not in list", friendly)
|
||||
}
|
||||
|
||||
func ensureWebhook(ctx context.Context, t *twilio) {
|
||||
pn, err := ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
|
||||
if err != nil {
|
||||
glog.Exitf("could not get our phone number: %v", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%ssms", flagWebhookPublic)
|
||||
|
||||
// first setup.
|
||||
if pn.SMSMethod != "POST" || pn.SMSURL != url {
|
||||
glog.Infof("Updating webhook (is %s %q, want %s %q)", pn.SMSMethod, pn.SMSURL, "POST", url)
|
||||
if err := t.updateIncomingPhoneNumberSMSWebhook(ctx, pn.SID, "POST", url); err != nil {
|
||||
glog.Exitf("could not set webhook: %v")
|
||||
}
|
||||
|
||||
// try again to check that it's actually set
|
||||
for {
|
||||
pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
|
||||
if err != nil {
|
||||
glog.Exitf("could not get our phone number: %v", err)
|
||||
}
|
||||
if pn.SMSMethod == "POST" || pn.SMSURL == url {
|
||||
break
|
||||
}
|
||||
glog.Infof("Webhook not yet ready, currently %s %q", pn.SMSMethod, pn.SMSURL)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
glog.Infof("Webhook verified")
|
||||
} else {
|
||||
glog.Infof("Webhook up to date")
|
||||
}
|
||||
|
||||
// now keep checking to make sure that nobody takes over our webhook
|
||||
tick := time.NewTicker(30 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick.C:
|
||||
pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
|
||||
if err != nil {
|
||||
glog.Exitf("could not get our phone number: %v", err)
|
||||
}
|
||||
if pn.SMSMethod != "POST" || pn.SMSURL != url {
|
||||
glog.Exitf("Webhook got deconfigured, not %s %q", pn.SMSMethod, pn.SMSURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
glog.Errorf("webhook body parse error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
accountSID := r.PostForm.Get("AccountSid")
|
||||
if accountSID != flagTwilioSID {
|
||||
glog.Errorf("webhook got wrong account sid, got %q, wanted %q", accountSID, flagTwilioSID)
|
||||
return
|
||||
}
|
||||
|
||||
body := r.PostForm.Get("Body")
|
||||
if body == "" {
|
||||
return
|
||||
}
|
||||
|
||||
from := r.PostForm.Get("From")
|
||||
|
||||
glog.Infof("Got SMS from %q, body %q", from, body)
|
||||
|
||||
s.dispatcher.publish(&sms{
|
||||
from: from,
|
||||
body: body,
|
||||
timestamp: time.Now(),
|
||||
})
|
||||
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&flagTwilioSID, "twilio_sid", "", "Twilio account SID")
|
||||
flag.StringVar(&flagTwilioToken, "twilio_token", "", "Twilio auth token")
|
||||
flag.StringVar(&flagTwilioFriendlyPhone, "twilio_friendly_phone", "", "Twilio friendly phone number")
|
||||
|
||||
flag.StringVar(&flagWebhookListen, "webhook_listen", "127.0.0.1:5000", "Listen address for webhook handler")
|
||||
flag.StringVar(&flagWebhookPublic, "webhook_public", "", "Public address for webhook handler (wg. http://proxy.q3k.org/smsgw/)")
|
||||
flag.Parse()
|
||||
|
||||
if flagTwilioSID == "" || flagTwilioToken == "" {
|
||||
glog.Exitf("twilio_sid and twilio_token must be set")
|
||||
}
|
||||
|
||||
if flagTwilioFriendlyPhone == "" {
|
||||
glog.Exitf("twilio_friendly_phone must be set")
|
||||
}
|
||||
|
||||
if flagWebhookPublic == "" {
|
||||
glog.Exitf("webhook_public must be set")
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(flagWebhookPublic, "/") {
|
||||
flagWebhookPublic += "/"
|
||||
}
|
||||
|
||||
s := &server{
|
||||
dispatcher: newDispatcher(),
|
||||
}
|
||||
|
||||
m := mirko.New()
|
||||
if err := m.Listen(); err != nil {
|
||||
glog.Exitf("Listen(): %v", err)
|
||||
}
|
||||
|
||||
webhookMux := http.NewServeMux()
|
||||
webhookMux.HandleFunc("/sms", s.webhookHandler)
|
||||
webhookSrv := http.Server{
|
||||
Addr: flagWebhookListen,
|
||||
Handler: webhookMux,
|
||||
}
|
||||
go func() {
|
||||
if err := webhookSrv.ListenAndServe(); err != nil {
|
||||
glog.Exitf("webhook ListenAndServe: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t := &twilio{
|
||||
accountSID: flagTwilioSID,
|
||||
accountToken: flagTwilioToken,
|
||||
}
|
||||
go ensureWebhook(m.Context(), t)
|
||||
go s.dispatcher.run(m.Context())
|
||||
|
||||
pb.RegisterSMSGatewayServer(m.GRPC(), s)
|
||||
|
||||
if err := m.Serve(); err != nil {
|
||||
glog.Exitf("Serve(): %v", err)
|
||||
}
|
||||
|
||||
<-m.Done()
|
||||
}
|
||||
|
||||
func (s *server) Messages(req *pb.MessagesRequest, stream pb.SMSGateway_MessagesServer) error {
|
||||
re := regexp.MustCompile(".*")
|
||||
if req.FilterBody != "" {
|
||||
var err error
|
||||
re, err = regexp.Compile(req.FilterBody)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "filter regexp error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
data := make(chan *sms)
|
||||
cancel := make(chan struct{})
|
||||
defer func() {
|
||||
close(cancel)
|
||||
close(data)
|
||||
}()
|
||||
|
||||
s.dispatcher.subscribe(&subscriber{
|
||||
re: re,
|
||||
data: data,
|
||||
cancel: cancel,
|
||||
})
|
||||
|
||||
for d := range data {
|
||||
stream.Send(&pb.MessagesResponse{
|
||||
Sender: d.from,
|
||||
Body: d.body,
|
||||
Timestamp: d.timestamp.UnixNano(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
||||
|
||||
proto_library(
|
||||
name = "proto_proto",
|
||||
srcs = ["smsgw.proto"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_proto_library(
|
||||
name = "proto_go_proto",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
|
||||
importpath = "code.hackerspace.pl/hscloud/hswaw/smsgw/proto",
|
||||
proto = ":proto_proto",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "proto",
|
||||
embed = [":proto_go_proto"],
|
||||
importpath = "code.hackerspace.pl/hscloud/hswaw/smsgw/proto",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
package proto
|
|
@ -1,17 +0,0 @@
|
|||
syntax = "proto3";
|
||||
package proto;
|
||||
option go_package = "code.hackerspace.pl/hscloud/hswaw/smsgw/proto";
|
||||
|
||||
message MessagesRequest {
|
||||
string filter_body = 1;
|
||||
}
|
||||
|
||||
message MessagesResponse {
|
||||
string sender = 1;
|
||||
string body = 3;
|
||||
int64 timestamp = 4;
|
||||
}
|
||||
|
||||
service SMSGateway {
|
||||
rpc Messages(MessagesRequest) returns (stream MessagesResponse);
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type twilio struct {
|
||||
accountSID string
|
||||
accountToken string
|
||||
}
|
||||
|
||||
type incomingPhoneNumber struct {
|
||||
FriendlyName string `json:"friendly_name"`
|
||||
SMSMethod string `json:"sms_method"`
|
||||
SMSURL string `json:"sms_url"`
|
||||
SID string `json:"sid"`
|
||||
}
|
||||
|
||||
func (t *twilio) getIncomingPhoneNumbers(ctx context.Context) ([]incomingPhoneNumber, error) {
|
||||
url := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/IncomingPhoneNumbers.json", t.accountSID)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.SetBasicAuth(t.accountSID, t.accountToken)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
result := struct {
|
||||
Message string `json:"message"`
|
||||
Status int64 `json:"status"`
|
||||
IPN []incomingPhoneNumber `json:"incoming_phone_numbers"`
|
||||
}{}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Message != "" {
|
||||
return nil, fmt.Errorf("REST response error, status: %v, message: %q", result.Status, result.Message)
|
||||
}
|
||||
|
||||
return result.IPN, nil
|
||||
}
|
||||
|
||||
func (t *twilio) updateIncomingPhoneNumberSMSWebhook(ctx context.Context, sid, method, whurl string) error {
|
||||
turl := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/IncomingPhoneNumbers/%s.json", t.accountSID, sid)
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("SmsMethod", method)
|
||||
data.Set("SmsUrl", whurl)
|
||||
|
||||
req, err := http.NewRequest("POST", turl, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.SetBasicAuth(t.accountSID, t.accountToken)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("status code: %v", res.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "voucherchecker_lib",
|
||||
srcs = ["main.go"],
|
||||
importpath = "code.hackerspace.pl/hscloud/hswaw/voucherchecker",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["@com_github_golang_glog//:glog"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "voucherchecker",
|
||||
embed = [":voucherchecker_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
container_layer(
|
||||
name = "layer_bin",
|
||||
directory = "/voucherchecker/",
|
||||
files = [
|
||||
":voucherchecker",
|
||||
],
|
||||
)
|
||||
|
||||
container_image(
|
||||
name = "runtime",
|
||||
base = "@prodimage-bionic//image",
|
||||
layers = [
|
||||
":layer_bin",
|
||||
],
|
||||
)
|
||||
|
||||
container_push(
|
||||
name = "push",
|
||||
format = "Docker",
|
||||
image = ":runtime",
|
||||
registry = "registry.k0.hswaw.net",
|
||||
repository = "q3k/voucherchecker",
|
||||
tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
|
||||
)
|
|
@ -1,241 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Set("logtostderr", "true")
|
||||
}
|
||||
|
||||
var (
|
||||
flagListen string
|
||||
|
||||
reVoucher = regexp.MustCompile("[A-Z0-9]+")
|
||||
)
|
||||
|
||||
type voucherstatus int
|
||||
|
||||
const (
|
||||
statusUnknown voucherstatus = iota
|
||||
statusInvalid
|
||||
statusUnused
|
||||
statusUsed
|
||||
statusCart
|
||||
)
|
||||
|
||||
func (v voucherstatus) String() string {
|
||||
switch v {
|
||||
case statusInvalid:
|
||||
return "INVALID"
|
||||
case statusUnused:
|
||||
return "UNUSED"
|
||||
case statusUsed:
|
||||
return "USED"
|
||||
case statusCart:
|
||||
return "INCART"
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
type vouchercache struct {
|
||||
status voucherstatus
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
func (c *vouchercache) fresh() bool {
|
||||
if c.status == statusUsed {
|
||||
return true
|
||||
}
|
||||
if c.expires.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type statusReq struct {
|
||||
voucher string
|
||||
res chan voucherstatus
|
||||
}
|
||||
|
||||
type refreshRes struct {
|
||||
voucher string
|
||||
status voucherstatus
|
||||
}
|
||||
|
||||
type service struct {
|
||||
statusReq chan *statusReq
|
||||
|
||||
pretixSem chan struct{}
|
||||
}
|
||||
|
||||
func newService() *service {
|
||||
return &service{
|
||||
statusReq: make(chan *statusReq),
|
||||
pretixSem: make(chan struct{}, 3),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) worker(ctx context.Context) error {
|
||||
cache := make(map[string]*vouchercache)
|
||||
waiters := make(map[string][]chan voucherstatus)
|
||||
refreshes := make(chan *refreshRes)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
||||
// is there a refresh pending?
|
||||
case ref := <-refreshes:
|
||||
glog.Infof("cache feed: %v is %v", ref.voucher, ref.status)
|
||||
expires := 30 * time.Minute
|
||||
if ref.status == statusInvalid {
|
||||
expires = 48 * time.Hour
|
||||
}
|
||||
cache[ref.voucher] = &vouchercache{
|
||||
status: ref.status,
|
||||
expires: time.Now().Add(expires),
|
||||
}
|
||||
for _, w := range waiters[ref.voucher] {
|
||||
w := w
|
||||
go func() {
|
||||
w <- ref.status
|
||||
}()
|
||||
}
|
||||
delete(waiters, ref.voucher)
|
||||
|
||||
// is there a new request?
|
||||
case req := <-s.statusReq:
|
||||
// return cache if fresh
|
||||
if el, ok := cache[req.voucher]; ok && el.fresh() {
|
||||
go func() {
|
||||
glog.Infof("cache hit: %v is %v", req.voucher, el.status)
|
||||
req.res <- el.status
|
||||
}()
|
||||
continue
|
||||
}
|
||||
// is someone waiting for a refresh already?
|
||||
if _, ok := waiters[req.voucher]; ok {
|
||||
glog.Infof("cache miss, secondary: %v", req.voucher)
|
||||
waiters[req.voucher] = append(waiters[req.voucher], req.res)
|
||||
continue
|
||||
}
|
||||
// request refresh
|
||||
glog.Infof("cache miss, primary: %v", req.voucher)
|
||||
waiters[req.voucher] = []chan voucherstatus{req.res}
|
||||
go func() {
|
||||
s := s.getStatus(ctx, req.voucher)
|
||||
refreshes <- &refreshRes{
|
||||
voucher: req.voucher,
|
||||
status: s,
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) run() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/status", s.handlerStatus)
|
||||
|
||||
ctx := context.Background()
|
||||
go s.worker(ctx)
|
||||
|
||||
glog.Infof("Listening on %s...", flagListen)
|
||||
if err := http.ListenAndServe(flagListen, mux); err != nil {
|
||||
glog.Exitf("could not listen: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) handlerStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := statusUnknown
|
||||
defer func() {
|
||||
e := json.NewEncoder(w)
|
||||
e.Encode(struct {
|
||||
Status string
|
||||
}{
|
||||
Status: status.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
voucher := r.URL.Query().Get("voucher")
|
||||
if voucher == "" || !strings.HasPrefix(voucher, "CHAOS") {
|
||||
status = statusInvalid
|
||||
return
|
||||
}
|
||||
if !reVoucher.MatchString(voucher) {
|
||||
status = statusInvalid
|
||||
return
|
||||
}
|
||||
|
||||
resC := make(chan voucherstatus)
|
||||
s.statusReq <- &statusReq{
|
||||
voucher: voucher,
|
||||
res: resC,
|
||||
}
|
||||
status = <-resC
|
||||
}
|
||||
|
||||
func (s *service) getStatus(ctx context.Context, voucher string) voucherstatus {
|
||||
s.pretixSem <- struct{}{}
|
||||
defer func() {
|
||||
<-s.pretixSem
|
||||
}()
|
||||
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
client := &http.Client{
|
||||
Jar: cookieJar,
|
||||
}
|
||||
|
||||
res, err := client.Get(fmt.Sprintf("https://tickets.events.ccc.de/36c3/redeem/?voucher=%s&subevent=&hello=this-is-q3k-at-hackerspace-pl-we-use-this-for-voucher-distribution", voucher))
|
||||
if err != nil {
|
||||
glog.Errorf("Getting main page: %v", err)
|
||||
return statusUnknown
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
glog.Errorf("Reading result page: %v", err)
|
||||
return statusUnknown
|
||||
}
|
||||
|
||||
if strings.Contains(string(data), "not known") {
|
||||
return statusInvalid
|
||||
}
|
||||
if strings.Contains(string(data), "already been") {
|
||||
return statusUsed
|
||||
}
|
||||
if strings.Contains(string(data), "You entered a voucher code that allows you ") {
|
||||
return statusUnused
|
||||
}
|
||||
if strings.Contains(string(data), "voucher code is currently locked") {
|
||||
return statusCart
|
||||
}
|
||||
|
||||
glog.Errorf("Unexpected result for %s", voucher)
|
||||
glog.Infof("%s", data)
|
||||
status := statusUnknown
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&flagListen, "listen", ":8081", "Listen address")
|
||||
flag.Parse()
|
||||
|
||||
s := newService()
|
||||
s.run()
|
||||
}
|
Loading…
Reference in a new issue