commit 5bc23daf4b2482a57e29c574802e6f8b318c8df7 Author: Erin Shepherd Date: Sat Nov 25 01:47:25 2023 +0100 Initial version diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..1547743 --- /dev/null +++ b/COPYING @@ -0,0 +1,5 @@ +Copyright 2023 Erin Shepherd + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..32e0a93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +ARG ALPINE=3.18 +FROM golang:1.21-alpine${ALPINE} AS build +WORKDIR /app +COPY go.mod go.sum . +RUN go mod download + +ADD . /app +RUN go build + +FROM alpine:${ALPINE} + +WORKDIR /bin +COPY --from=build /app/matrix-vanity . + +WORKDIR / +CMD ["/bin/matrix-vanity"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..872d1db --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Matrix-Vanity - Vanity names for Matrix rooms + +This is an absolute bare minimum Matrix server implementation designed +to do one thing and one thing only: Enable you to give rooms vanity names +without running a homeserver + +It handles just the room lookup by name endpoint, which is all that is needed +for this purpose. Perhaps in the future it will implement the room directory +as well, but for now it does not. + +## Installation +Pick a hostname and port that you will run this on. Organize for matrix-vanity +to run behind a TLS terminating proxy (it does not handle TLS itself) + +Create a JSON file defining rooms: +```js +{ + "#matrix:example.com": { // <-- Alias + "room_id": "!OGEhHVWSdvArJzumhm:matrix.org", // Room ID, from the Advanced tab of the room settings in Element + "servers": ["matrix.org"] // Pick one or more of the homeservers which host the room + } +} + +``` + +When starting the server, set the following environment variables: + +* `MXV_ROOMS_FILE` to the path to your room definition file, +* `MXV_USE_PROXY_HEADERS` if you wish to have the service interpret X-Forwarded-For + (only used for log output) +* `MXV_LISTEN` to your preferred listening spec (Default: `:3333`) + +Check things are running, and that you have TLS certificates configured. + +Delegate Matrix server-to-server handling for your hostname to this instance +[per the spec](https://spec.matrix.org/v1.8/server-server-api/#resolving-server-names), +by setting up `/.well-known/matrix/server` (or picking one of the other methods if you +prefer) + +```json +{ + "m.server": "matrix-vanity.example.com:443" +} +``` +(Remember that if you don't specify a port, the default is 8448) + +Now for each of your rooms for which you wish to advertise this alias, go to the room's +Visibility settings page in Element and enter the address in the "Other published addresses" +field. If all is configured properly, it should turn green. You can then make it the main +address if you wish. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8f169d5 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.alioth.systems/erin/matrix-vanity + +go 1.20 + +require ( + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/gorilla/handlers v1.5.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fdb818f --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d4f0b7a --- /dev/null +++ b/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/gorilla/handlers" +) + +type mxError struct { + ErrCode string `json:"errcode"` + Error string `json:"error"` +} + +var internalErrorBuf = []byte(`{"errcode":"M_UNKNOWN", "error": "Internal error"}`) + +type RoomMapping struct { + RoomID string `json:"room_id"` + Servers []string `json:"servers"` +} + +var KnownRooms = map[string]RoomMapping{} + +func SendJSON(w http.ResponseWriter, obj any, status int) { + w.Header().Set("Content-Type", "application/json") + buf, err := json.Marshal(obj) + if err != nil { + log.Printf("Error marshaling JSON response: %v", err) + w.WriteHeader(500) + _, _ = w.Write(internalErrorBuf) + return + } + + w.WriteHeader(status) + _, _ = w.Write(buf) +} + +func NotFound(w http.ResponseWriter, r *http.Request) { + SendJSON(w, mxError{"M_UNRECOGNIZED", "Unsupported Endpoint"}, 404) +} + +func MethodNotAllowed(w http.ResponseWriter, r *http.Request) { + SendJSON(w, mxError{"M_UNRECOGNIZED", "Unrecognized Method"}, 405) +} + +func Version(w http.ResponseWriter, r *http.Request) { + SendJSON(w, map[string]any{ + "name": "Matrix-Vanity", + "version": "0.0", + }, 200) +} + +func QueryDirectory(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + roomAlias := q.Get("room_alias") + + mapping, ok := KnownRooms[roomAlias] + if !ok { + log.Printf("Room '%s' not found", roomAlias) + SendJSON(w, mxError{"M_NOT_FOUND", "Room alias not found."}, 404) + return + } + + SendJSON(w, mapping, 200) +} + +func Index(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("This is a Matrix-Vanity server")) +} + +func main() { + useProxyIP := os.Getenv("MXV_USE_PROXY_HEADERS") != "" + + configFile := os.Getenv("MXV_ROOMS_FILE") + if configFile == "" { + configFile = "/etc/matrix-vanity-rooms.json" + } + log.Printf("Loading room list from %s", configFile) + + listenAddr := os.Getenv("MXV_LISTEN") + if listenAddr == "" { + listenAddr = ":3333" + } + + buf, err := os.ReadFile(configFile) + if err != nil { + log.Fatalf("Error reading room file: %v", err) + } + + if err := json.Unmarshal(buf, &KnownRooms); err != nil { + log.Fatalf("Error parsing room file: %v", err) + } + + log.Print("Defined Rooms:") + for alias, info := range KnownRooms { + log.Printf(" - '%s', ID: '%s', Servers: %v", alias, info.RoomID, info.Servers) + } + if len(KnownRooms) == 0 { + log.Printf("⚠️ No rooms defined. That doesn't seem very useful ") + } + log.Print("") + + r := chi.NewRouter() + if useProxyIP { + log.Print("X-Forwarded-For header intepretation enabled; it is assumed you are running this service behind a proxy") + r.Use(handlers.ProxyHeaders) + } + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + r.NotFound(NotFound) + r.MethodNotAllowed(MethodNotAllowed) + r.Get("/", Index) + r.Get("/_matrix/federation/v1/version", Version) + r.Get("/_matrix/federation/v1/query/directory", QueryDirectory) + log.Printf("Listening on %q", listenAddr) + http.ListenAndServe(listenAddr, r) +}