diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3c4900 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# PLC +/plc/did-method-plc \ No newline at end of file diff --git a/README.md b/README.md index f0371b8..1592ee1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ # localdev -Code and configuration to create a local development environment. \ No newline at end of file +Code and configuration to create a network-local development environment. It uses tailscale and can be used to have shared isolated infrastructure that can be used to support individuals and teams. + +## Configuration + + +## Operation + +1. Configure and start the PLC service. See plc/README.md + +2. Configure and start the PDS service. See pds/README.md + +3. Configure and start the DNS service. See dns/README.md + +4. Configure split-DNS in Tailscale. + + 1. Visit https://tailscale.com/ + 2. Go to the Machines tab and get the internal IP address of `didadmin` + 2. Go to the DNS configuration page + 3. Add a nameserver and select "Custom" + 4. Enter the IP address of the `didadmin`, select "Restrict to domain (Split DNS)", and set the domain to "pyroclastic.cloud" + +## Maintenance + +Tailscale SSL certificates need to be periodically regenerated. Run the respective `docker compose exec tailscale /bin/sh -c "tailscale cert ..."` command to generate new certs and restart (stop and start) the nginx proxy for it to use the new cert. diff --git a/dns/Corefile.example b/dns/Corefile.example new file mode 100644 index 0000000..367695c --- /dev/null +++ b/dns/Corefile.example @@ -0,0 +1,14 @@ +. { + log + errors + + reload 10s + + records pyroclastic.cloud { + @ 60 IN TXT "TEST" + _atproto.test1734305850 60 IN TXT "did=did:plc:p75ngbyvabgetgoy52aswele" + _atproto.test1734440080 60 IN TXT "did=did:plc:k5d6h7nlhbh5tuxrlxczgal3" + _atproto.test1734440644 60 IN TXT "did=did:plc:x45wmz7vktj2aqcqwj7yakxs" + } + +} diff --git a/dns/README.md b/dns/README.md new file mode 100644 index 0000000..6e57f07 --- /dev/null +++ b/dns/README.md @@ -0,0 +1,50 @@ +# DNS + +The DNS component does several things: + +1. It uses CoreDNS as a split-DNS nameserver for resolving local handles. +2. It provides a small HTTP application for generating new handles for testing purposes. + +## Configuration + +This service makes API calls to the local PDS and also exists on a tailscale network. Please make note of any `PLACEHOLDER` and `OPTIONAL` strings in the following files: + +In `./docker-compose.yml`: + +* Set the `PDS_ADMIN_PASSWORD` environment variable to your PDS admin password. +* Set the `PDS_HOSTNAME` to the internal hostname of your PDS. (i.e. `pds.sneaky-fox.ts.net`) +* Optionally, if you are not using the `pyroclastic.cloud` domain (it's fine to leave this as-is) then change that. + +## Operation + +1. First, build the `didadmin` tool. + + `docker build -f ./didadmin/Dockerfile -t didadmin ./didadmin/` + +3. Bring networking up. + + `docekr compose up tailscale -d` + + If you are using dynamic node registration, you'll need to view the logs and click on the link. + + `docker compose logs tailscale` + +4. Generate an SSL certificate for the node. Be sure to change `internal.ts.net` to whatever your Tailnet name is (i.e. `sneaky-fox.ts.net`) + + `docker compose exec tailscale /bin/sh -c "tailscale cert --cert-file /mnt/tls/cert.pem --key-file /mnt/tls/cert.key didadmin.internal.ts.net"` + +5. Bring didadmin up. + + `docekr compose up app -d` + + When this first starts, it'll create the `/etc/coredns/database.db` and `/etc/coredns/Corefile` files inside the container. + +6. Bring coredns and the proxy up. + + `docker compose up -d` + +7. Ensure the PLC and PDS services are running, and split-DNS is configured before using. + +## Usage + +In a browser, visit https://didadmin.sneaky-fox.ts.net/ and use the form to create accounts on the local PDS. \ No newline at end of file diff --git a/dns/didadmin/Dockerfile b/dns/didadmin/Dockerfile new file mode 100644 index 0000000..4af4180 --- /dev/null +++ b/dns/didadmin/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:alpine3.21 AS build +ENV CGO_ENABLED=1 +RUN apk add --no-cache gcc musl-dev +WORKDIR /workspace +COPY go.mod /workspace/ +COPY go.sum /workspace/ +RUN go mod download +COPY main.go /workspace/ +ENV GOCACHE=/root/.cache/go-build +RUN --mount=type=cache,target="/root/.cache/go-build" go install -ldflags='-s -w -extldflags "-static"' ./main.go + +FROM scratch +COPY --from=build /go/bin/main /usr/local/bin/didadmin +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +ENTRYPOINT [ "/usr/local/bin/didadmin" ] diff --git a/dns/didadmin/go.mod b/dns/didadmin/go.mod new file mode 100644 index 0000000..5d7d701 --- /dev/null +++ b/dns/didadmin/go.mod @@ -0,0 +1,27 @@ +module github.com/astrenoxcoop/magicdev-admin + +go 1.23.5 + +require ( + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/sethvargo/go-envconfig v1.1.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.17 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect + go.etcd.io/etcd/client/v3 v3.5.17 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/dns/didadmin/go.sum b/dns/didadmin/go.sum new file mode 100644 index 0000000..1a34bc2 --- /dev/null +++ b/dns/didadmin/go.sum @@ -0,0 +1,83 @@ +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= +github.com/sethvargo/go-envconfig v1.1.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= +go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= +go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= +go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= +go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= +go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dns/didadmin/main.go b/dns/didadmin/main.go new file mode 100644 index 0000000..e368ac3 --- /dev/null +++ b/dns/didadmin/main.go @@ -0,0 +1,265 @@ +package main + +import ( + "bytes" + "context" + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "text/template" + + petname "github.com/dustinkirkland/golang-petname" + _ "github.com/mattn/go-sqlite3" + "github.com/sethvargo/go-envconfig" +) + +type ServerConfig struct { + Database string `env:"DATABASE, default=./database.db"` + PDSHostName string `env:"PDS_HOSTNAME, default=pds.internal.ts.net"` + PDSAdminPassword string `env:"PDS_ADMIN_PASSWORD, required"` + Domain string `env:"DOMAIN, default=pyroclastic.cloud"` + Corefile string `env:"COREFILE, default=./Corefile"` +} + +type createInviteResponse struct { + Code string `json:"code"` +} + +type createAccountRequest struct { + Email string `json:"email"` + Handle string `json:"handle"` + Password string `json:"password"` + InviteCode string `json:"inviteCode"` +} + +type createAccountResponse struct { + DID string `json:"did"` +} + +func createInvite(server, password string) (string, error) { + url := fmt.Sprintf("https://%s/xrpc/com.atproto.server.createInviteCode", server) + + requestBody := []byte(`{"useCount":1}`) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:"+password))) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + + decoder := json.NewDecoder(resp.Body) + var createInvite createInviteResponse + err = decoder.Decode(&createInvite) + if err != nil { + return "", err + } + + return createInvite.Code, nil +} + +func createAccount(server, password, inviteCode, handle, email string) (string, error) { + url := fmt.Sprintf("https://%s/xrpc/com.atproto.server.createAccount", server) + + createAccountRequestBody := createAccountRequest{ + Email: email, + Handle: handle, + Password: "password", + InviteCode: inviteCode, + } + requestBody, err := json.Marshal(createAccountRequestBody) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:"+password))) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + + decoder := json.NewDecoder(resp.Body) + var createdAccount createAccountResponse + err = decoder.Decode(&createdAccount) + if err != nil { + return "", err + } + + return createdAccount.DID, nil +} + +type handlers struct { + config *ServerConfig + db *sql.DB +} + +func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) { + handle := r.PostFormValue("handle") + + if handle == "" { + handle = petname.Generate(2, "-") + body := fmt.Sprintf(`
`, handle) + io.WriteString(w, body) + return + } + + inviteCode, err := createInvite(h.config.PDSHostName, h.config.PDSAdminPassword) + if err != nil { + io.WriteString(w, fmt.Sprintf("Error: %s", err)) + return + } + + email := fmt.Sprintf("%s@%s", handle, h.config.Domain) + full_handle := fmt.Sprintf("%s.%s", handle, h.config.Domain) + + did, err := createAccount(h.config.PDSHostName, h.config.PDSAdminPassword, inviteCode, full_handle, email) + if err != nil { + io.WriteString(w, fmt.Sprintf("Error: %s", err)) + return + } + + _, err = h.db.Exec(`INSERT INTO handles (did, handle) VALUES (?, ?)`, &did, &handle) + if err != nil { + io.WriteString(w, fmt.Sprintf("Error: %s", err)) + return + } + + if err = generateCorefile(h.config, h.db); err != nil { + io.WriteString(w, fmt.Sprintf("Error: %s", err)) + return + } + + body := fmt.Sprintf(`Created %s with handle %s
Back`, did, full_handle) + + io.WriteString(w, body) +} + +func (h *handlers) newHandler(w http.ResponseWriter, r *http.Request) { + handle := r.PostFormValue("handle") + if handle == "" { + handle = "testy" + } + + io.WriteString(w, fmt.Sprintf("Hello, %s!\n", handle)) +} + +func generateCorefile(config *ServerConfig, db *sql.DB) error { + corefileTemplate := ` +. { + log + errors + reload 10s + records {{ .Domain }} { + @ 60 IN TXT "TEST" +{{ range .Records }} + _atproto.{{ .Handle }} 60 IN TXT "did={{ .DID }}"{{ end }} + } +}` + + corefile, err := template.New("corefile").Parse(corefileTemplate) + if err != nil { + log.Fatal(err) + } + + type corefileValueRecord struct { + DID string + Handle string + } + type corefileValues struct { + Domain string + Records []corefileValueRecord + } + + records := make([]corefileValueRecord, 0) + + rows, err := db.Query("SELECT handle, did FROM handles") + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var handle string + var did string + err = rows.Scan(&handle, &did) + if err != nil { + return err + } + records = append(records, corefileValueRecord{did, handle}) + } + err = rows.Err() + if err != nil { + return err + } + data := corefileValues{ + Domain: config.Domain, + Records: records, + } + + output, err := os.Create(config.Corefile) + if err != nil { + return err + } + defer output.Close() + + err = corefile.Execute(output, data) + if err != nil { + log.Fatal(err) + } + return nil +} + +func main() { + ctx := context.Background() + + var config ServerConfig + if err := envconfig.Process(ctx, &config); err != nil { + log.Fatal(err) + } + + db, err := sql.Open("sqlite3", config.Database) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS handles (did TEXT NOT NULL PRIMARY KEY, handle TEXT NOT NULL UNIQUE)`) + if err != nil { + log.Fatal(err) + } + + h := handlers{ + config: &config, + db: db, + } + + if err = generateCorefile(h.config, h.db); err != nil { + log.Fatal(err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/", h.indexHandler) + mux.HandleFunc("/new", h.newHandler) + if err = http.ListenAndServe(":3333", mux); !errors.Is(err, http.ErrServerClosed) { + log.Fatal(err) + } + +} diff --git a/dns/docker-compose.yml b/dns/docker-compose.yml new file mode 100644 index 0000000..3a366a6 --- /dev/null +++ b/dns/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' +volumes: + dns_db: + dns_ts: + dns_tls: + dns_coredns: +services: + coredns: + image: coredns + network_mode: service:tailscale + restart: on-failure + volumes: + - dns_coredns:/etc/coredns/ + entrypoint: /coredns + command: -conf /etc/coredns/Corefile + app: + image: "didadmin" + restart: unless-stopped + environment: + - PDS_ADMIN_PASSWORD=PLACEHOLDER + - DATABASE=/etc/coredns/database.db + - PDS_HOSTNAME=PLACEHOLDER pds.internal.ts.net + - DOMAIN=pyroclastic.cloud + - COREFILE=/etc/coredns/Corefile + volumes: + - dns_coredns:/etc/coredns/ + tailscale: + image: tailscale/tailscale:latest + restart: unless-stopped + environment: + # OPTIONAL - TS_AUTHKEY=YOUR-TS-KEY-GOES-HERE + - TS_STATE_DIR=/var/run/tailscale + - TS_HOSTNAME=didadmin + volumes: + - dns_tls:/mnt/tls + - dns_ts:/var/run/tailscale + nginx: + image: nginx + restart: unless-stopped + network_mode: service:tailscale + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - dns_tls:/mnt/tls:ro diff --git a/dns/nginx.conf b/dns/nginx.conf new file mode 100644 index 0000000..45687c7 --- /dev/null +++ b/dns/nginx.conf @@ -0,0 +1,17 @@ +events {} +http { + server { + resolver 127.0.0.11 [::1]:5353 valid=15s; + set $backend "http://app:3333"; + listen 443 ssl; + ssl_certificate /mnt/tls/cert.pem; + ssl_certificate_key /mnt/tls/cert.key; + location / { + proxy_pass $backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + client_max_body_size 64M; + } + } +} diff --git a/pds/README.md b/pds/README.md new file mode 100644 index 0000000..8adbcd6 --- /dev/null +++ b/pds/README.md @@ -0,0 +1,41 @@ +# PDS + +## Configuration + +This is a fully operational PDS and needs appropriate configuration. If you decide to run multiple PDS instances for testing, be sure to configure each one individually. + +Copy the `env.example` file to `env` and update the following entry "PLACEHOLDER" values. + +* `PDS_JWT_SECRET` value set with `openssl rand --hex 16` +* `PDS_ADMIN_PASSWORD` value set with `openssl rand --hex 16` +* `PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX` value set with `openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32` +* `PDS_HOSTNAME` value updated to relflect your internal tailnet +* `PDS_ADMIN_EMAIL` value updated to relflect your internal tailnet +* `PDS_DID_PLC_URL` value updated to relflect your internal tailnet +* Optionally, if you are not using the `pyroclastic.cloud` domain (it's fine to leave this as-is) then change that. + +## Operation + +1. Create the configuration file and update it accordingly. + +2. Bring networking up. + + `docekr compose up tailscale -d` + + If you are using dynamic node registration, you'll need to view the logs and click on the link. + + `docker compose logs tailscale` + +3. Generate an SSL certificate for the node. Be sure to change `internal.ts.net` to whatever your Tailnet name is (i.e. `sneaky-fox.ts.net`) + + `docker compose exec tailscale /bin/sh -c "tailscale cert --cert-file /mnt/tls/cert.pem --key-file /mnt/tls/cert.key pds.internal.ts.net"` + +4. Bring the app and proxy up. + + `docker compose up -d` + +## Usage + +The PDS will be available at https://pds.internal.ts.net/. + +The maildev service will be available at http://pds.internal.ts.net:1080/. diff --git a/pds/docker-compose.yml b/pds/docker-compose.yml new file mode 100644 index 0000000..58284f4 --- /dev/null +++ b/pds/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' +volumes: + pds_data: + pds_ts: + pds_tls: +services: + maildev: + image: maildev/maildev + restart: unless-stopped + app: + image: ghcr.io/bluesky-social/pds:0.4 + restart: unless-stopped + env_file: "env" + volumes: + - pds_data:/pds + tailscale: + image: tailscale/tailscale:latest + restart: unless-stopped + environment: + # OPTIONAL - TS_AUTHKEY=YOUR-TS-KEY-GOES-HERE + - TS_STATE_DIR=/var/run/tailscale + - TS_HOSTNAME=pds + volumes: + - pds_tls:/mnt/tls + - pds_ts:/var/run/tailscale + nginx: + image: nginx + restart: unless-stopped + network_mode: service:tailscale + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - pds_tls:/mnt/tls:ro diff --git a/pds/env.example b/pds/env.example new file mode 100644 index 0000000..5279c7c --- /dev/null +++ b/pds/env.example @@ -0,0 +1,20 @@ +PDS_SERVICE_HANDLE_DOMAINS=.pyroclastic.cloud +PDS_HOSTNAME=pds.internal.ts.net +PDS_JWT_SECRET=PLACEHOLDER +PDS_ADMIN_PASSWORD=PLACEHOLDER +PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=PLACEHOLDER +PDS_ADMIN_EMAIL=admin@plc.internal.ts.net +PDS_DATA_DIRECTORY=/pds +PDS_BLOBSTORE_DISK_LOCATION=/pds/blobs +LOG_ENABLED=true +PDS_DID_PLC_URL=https://plc.internal.ts.net +PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app +PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac +PDS_EMAIL_FROM_ADDRESS=postmaster@localhost +PDS_EMAIL_SMTP_URL=smtp://maildev:1025 +PDS_ACCEPTING_REPO_IMPORTS=true +PDS_DEV_MODE=TRUE +DEBUG_MODE=TRUE +LOG_LEVEL=trace +PDS_PORT=3001 +PDS_BLOB_UPLOAD_LIMIT=52428800 \ No newline at end of file diff --git a/pds/nginx.conf b/pds/nginx.conf new file mode 100644 index 0000000..a91302f --- /dev/null +++ b/pds/nginx.conf @@ -0,0 +1,17 @@ +events {} +http { + server { + resolver 127.0.0.11 [::1]:5353 valid=15s; + set $backend "http://app:3001"; + listen 443 ssl; + ssl_certificate /mnt/tls/cert.pem; + ssl_certificate_key /mnt/tls/cert.key; + location / { + proxy_pass $backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + client_max_body_size 64M; + } + } +} diff --git a/plc/README.md b/plc/README.md new file mode 100644 index 0000000..df4e432 --- /dev/null +++ b/plc/README.md @@ -0,0 +1,31 @@ +# PLC + +To start a PLC server, you must build a container from the PLC repository. + +1. First, clone https://github.com/did-method-plc/did-method-plc + + `git clone https://github.com/did-method-plc/did-method-plc` + +2. Build the container + + `docker build -f ./did-method-plc/packages/server/Dockerfile -t plcjs ./did-method-plc/` + +3. Bring networking up. + + `docekr compose up tailscale -d` + + If you are using dynamic node registration, you'll need to view the logs and click on the link. + + `docker compose logs tailscale` + +4. Generate an SSL certificate for the node. Be sure to change `internal.ts.net` to whatever your Tailnet name is (i.e. `sneaky-fox.ts.net`) + + `docker compose exec tailscale /bin/sh -c "tailscale cert --cert-file /mnt/tls/cert.pem --key-file /mnt/tls/cert.key plc.internal.ts.net"` + +5. Bring the database up. + + `docekr compose up db -d` + +6. Bring the app and proxy up. + + `docker compose up -d` diff --git a/plc/docker-compose.yml b/plc/docker-compose.yml new file mode 100644 index 0000000..f94799b --- /dev/null +++ b/plc/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.8' +volumes: + plc_db: + plc_ts: + plc_tls: +services: + db: + image: postgres:14.4-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=pg + - POSTGRES_PASSWORD=password + healthcheck: + test: 'pg_isready -U pg' + interval: 500ms + timeout: 10s + retries: 20 + volumes: + - plc_db:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + app: + depends_on: + db: + condition: service_healthy + restart: true + image: plcjs + restart: unless-stopped + environment: + - DATABASE_URL=postgres://pg:password@db/plc + - DEBUG_MODE=1 + - LOG_ENABLED=true + - LOG_LEVEL=debug + - DB_CREDS_JSON={"username":"pg","password":"password","host":"db","port":"5432","database":"plc"} + - DB_MIGRATE_CREDS_JSON={"username":"pg","password":"password","host":"db","port":"5432","database":"plc"} + - ENABLE_MIGRATIONS=true + - LOG_DESTINATION=1 + ports: + - '3000:3000' + tailscale: + image: tailscale/tailscale:latest + restart: unless-stopped + environment: + # OPTIONAL - TS_AUTHKEY=YOUR-TS-KEY-GOES-HERE + - TS_STATE_DIR=/var/run/tailscale + - TS_HOSTNAME=plc + volumes: + - plc_tls:/mnt/tls + - plc_ts:/var/run/tailscale + nginx: + image: nginx + restart: unless-stopped + network_mode: service:tailscale + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - plc_tls:/mnt/tls:ro diff --git a/plc/init.sql b/plc/init.sql new file mode 100644 index 0000000..9a4404c --- /dev/null +++ b/plc/init.sql @@ -0,0 +1,3 @@ +-- plc +CREATE DATABASE plc; +GRANT ALL PRIVILEGES ON DATABASE plc TO pg; diff --git a/plc/nginx.conf b/plc/nginx.conf new file mode 100644 index 0000000..075dd1f --- /dev/null +++ b/plc/nginx.conf @@ -0,0 +1,17 @@ +events {} +http { + server { + resolver 127.0.0.11 [::1]:5353 valid=15s; + set $backend "http://app:3000"; + listen 443 ssl; + ssl_certificate /mnt/tls/cert.pem; + ssl_certificate_key /mnt/tls/cert.key; + location / { + proxy_pass $backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + client_max_body_size 64M; + } + } +}