1
0
Fork 0

*: 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>
master
radex 2023-10-28 23:58:45 +02:00 committed by radex
parent 633fb2e8ce
commit caf65fcaaf
22 changed files with 12 additions and 1518 deletions

11
CEMETERY.md Normal file
View 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.

View File

@ -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

View File

@ -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; }
",
}
}
},
}

View File

@ -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",
},
},
}

View File

@ -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" },

View File

@ -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",
},
},
},
},
},
}

View File

@ -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",

View File

@ -1,40 +0,0 @@
-----BEGIN PGP MESSAGE-----
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
=SBGB
-----END PGP MESSAGE-----

View File

@ -1,40 +0,0 @@
-----BEGIN PGP MESSAGE-----
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
=hseZ
-----END PGP MESSAGE-----

View File

@ -1,40 +0,0 @@
-----BEGIN PGP MESSAGE-----
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
=bRrv
-----END PGP MESSAGE-----

View File

@ -1,40 +0,0 @@
-----BEGIN PGP MESSAGE-----
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
WtNaMpCfU92nkD2ZwTp96oiLUOVbWEaGGByh
=IR9z
-----END PGP MESSAGE-----

View File

@ -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"),
}
},
}

View File

@ -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}",
)

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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"],
)

View File

@ -1 +0,0 @@
package proto

View File

@ -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);
}

View File

@ -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
}

View File

@ -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}",
)

View File

@ -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()
}