From cf4d8e99ea074bd2540fa3e1843724d6306af8c2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Feb 2021 01:58:40 -0600 Subject: [PATCH 1/7] Add application service support to blueprints Split out from https://github.com/matrix-org/complement/pull/68 --- dockerfiles/synapse/homeserver.yaml | 7 + dockerfiles/synapse/start.sh | 15 +- internal/b/blueprints.go | 56 ++++++- internal/b/hs_with_application_service.go | 25 ++++ internal/docker/builder.go | 175 +++++++++++++++++----- internal/docker/deployer.go | 4 +- internal/docker/deployment.go | 16 +- 7 files changed, 250 insertions(+), 48 deletions(-) create mode 100644 internal/b/hs_with_application_service.go diff --git a/dockerfiles/synapse/homeserver.yaml b/dockerfiles/synapse/homeserver.yaml index a2f6f309..9fd6c97a 100644 --- a/dockerfiles/synapse/homeserver.yaml +++ b/dockerfiles/synapse/homeserver.yaml @@ -92,6 +92,13 @@ rc_joins: federation_rr_transactions_per_room_per_second: 9999 +## API Configuration ## + +# A list of application service config files to use +# +app_service_config_files: +AS_REGISTRATION_FILES + ## Experimental Features ## experimental_features: diff --git a/dockerfiles/synapse/start.sh b/dockerfiles/synapse/start.sh index 6e35b4dd..8af7c278 100755 --- a/dockerfiles/synapse/start.sh +++ b/dockerfiles/synapse/start.sh @@ -1,9 +1,22 @@ -#!/bin/sh +#!/bin/bash set -e sed -i "s/SERVER_NAME/${SERVER_NAME}/g" /conf/homeserver.yaml +# Add the application service registration files to the homeserver.yaml config +for filename in /appservices/*.yaml; do + [ -f "$filename" ] || break + + as_id=$(basename "$filename" .yaml) + + # Insert the path to the registration file and the AS_REGISTRATION_FILES marker after + # so we can add the next application service in the next iteration of this for loop + sed -i "s/AS_REGISTRATION_FILES/ - \/appservices\/${as_id}.yaml\nAS_REGISTRATION_FILES/g" /conf/homeserver.yaml +done +# Remove the AS_REGISTRATION_FILES entry +sed -i "s/AS_REGISTRATION_FILES//g" /conf/homeserver.yaml + # generate an ssl cert for the server, signed by our dummy CA openssl req -new -key /conf/server.tls.key -out /conf/server.tls.csr \ -subj "/CN=${SERVER_NAME}" diff --git a/internal/b/blueprints.go b/internal/b/blueprints.go index ef8424b4..48223b0b 100644 --- a/internal/b/blueprints.go +++ b/internal/b/blueprints.go @@ -15,9 +15,13 @@ package b import ( + "crypto/rand" + "encoding/hex" "fmt" "strconv" "strings" + + "github.com/sirupsen/logrus" ) // KnownBlueprints lists static blueprints @@ -26,6 +30,7 @@ var KnownBlueprints = map[string]*Blueprint{ BlueprintAlice.Name: &BlueprintAlice, BlueprintFederationOneToOneRoom.Name: &BlueprintFederationOneToOneRoom, BlueprintFederationTwoLocalOneRemote.Name: &BlueprintFederationTwoLocalOneRemote, + BlueprintHSWithApplicationService.Name: &BlueprintHSWithApplicationService, BlueprintOneToOneRoom.Name: &BlueprintOneToOneRoom, BlueprintPerfManyMessages.Name: &BlueprintPerfManyMessages, BlueprintPerfManyRooms.Name: &BlueprintPerfManyRooms, @@ -46,6 +51,8 @@ type Homeserver struct { Users []User // The list of rooms to create on this homeserver Rooms []Room + // The list of application services to create on the homeserver + ApplicationServices []ApplicationService } type User struct { @@ -68,11 +75,22 @@ type Room struct { Events []Event } +type ApplicationService struct { + ID string + HSToken string + ASToken string + URL string + SenderLocalpart string + RateLimited bool +} + type Event struct { - Type string - Sender string - StateKey *string - Content map[string]interface{} + Type string + Sender string + OriginServerTS uint64 + StateKey *string + PrevEvents []string + Content map[string]interface{} // This field is ignored in blueprints as clients are unable to set it. Used with federation.Server Unsigned map[string]interface{} } @@ -107,7 +125,18 @@ func Validate(bp Blueprint) (Blueprint, error) { return bp, err } } + for i, as := range hs.ApplicationServices { + hs.ApplicationServices[i], err = normalizeApplicationService(as) + if err != nil { + return bp, err + } + } } + + logrus.WithFields(logrus.Fields{ + "bp": bp.Homeservers[0].ApplicationServices, + }).Error("after modfiying bp") + return bp, nil } @@ -152,6 +181,25 @@ func normaliseUser(u string, hsName string) (string, error) { return u, nil } +func normalizeApplicationService(as ApplicationService) (ApplicationService, error) { + hsToken := make([]byte, 32) + _, err := rand.Read(hsToken) + if err != nil { + return as, err + } + + asToken := make([]byte, 32) + _, err = rand.Read(asToken) + if err != nil { + return as, err + } + + as.HSToken = hex.EncodeToString(hsToken) + as.ASToken = hex.EncodeToString(asToken) + + return as, err +} + // Ptr returns a pointer to `in`, because Go doesn't allow you to inline this. func Ptr(in string) *string { return &in diff --git a/internal/b/hs_with_application_service.go b/internal/b/hs_with_application_service.go new file mode 100644 index 00000000..ba9db923 --- /dev/null +++ b/internal/b/hs_with_application_service.go @@ -0,0 +1,25 @@ +package b + +// BlueprintHSWithApplicationService who has an application service to interact with +var BlueprintHSWithApplicationService = MustValidate(Blueprint{ + Name: "alice", + Homeservers: []Homeserver{ + { + Name: "hs1", + Users: []User{ + { + Localpart: "@alice", + DisplayName: "Alice", + }, + }, + ApplicationServices: []ApplicationService{ + { + ID: "my_as_id", + URL: "http://localhost:9000", + SenderLocalpart: "the-bridge-user", + RateLimited: false, + }, + }, + }, + }, +}) diff --git a/internal/docker/builder.go b/internal/docker/builder.go index a8b45ce6..619ad653 100644 --- a/internal/docker/builder.go +++ b/internal/docker/builder.go @@ -14,6 +14,8 @@ package docker import ( + "archive/tar" + "bytes" "context" "errors" "fmt" @@ -31,6 +33,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/volume" client "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" @@ -276,6 +279,12 @@ func (d *Builder) construct(bprint b.Blueprint) (errs []error) { } labels := labelsForTokens(runner.AccessTokens(res.homeserver.Name)) + // Combine the labels for tokens and application services + asLabels := labelsForApplicationServices(res.homeserver) + for k, v := range asLabels { + labels[k] = v + } + // commit the container commit, err := d.Docker.ContainerCommit(context.Background(), res.containerID, types.ContainerCommitOptions{ Author: "Complement", @@ -300,7 +309,7 @@ func (d *Builder) construct(bprint b.Blueprint) (errs []error) { func (d *Builder) constructHomeserver(blueprintName string, runner *instruction.Runner, hs b.Homeserver, networkID string) result { contextStr := fmt.Sprintf("%s.%s", blueprintName, hs.Name) d.log("%s : constructing homeserver...\n", contextStr) - dep, err := d.deployBaseImage(blueprintName, hs.Name, contextStr, networkID) + dep, err := d.deployBaseImage(blueprintName, hs, contextStr, networkID) if err != nil { log.Printf("%s : failed to deployBaseImage: %s\n", contextStr, err) containerID := "" @@ -328,17 +337,19 @@ func (d *Builder) constructHomeserver(blueprintName string, runner *instruction. } // deployBaseImage runs the base image and returns the baseURL, containerID or an error. -func (d *Builder) deployBaseImage(blueprintName, hsName, contextStr, networkID string) (*HomeserverDeployment, error) { +func (d *Builder) deployBaseImage(blueprintName string, hs b.Homeserver, contextStr, networkID string) (*HomeserverDeployment, error) { + asIDToRegistrationMap := asIDToRegistrationFromLabels(labelsForApplicationServices(hs)) + return deployImage( - d.Docker, d.BaseImage, d.CSAPIPort, fmt.Sprintf("complement_%s", contextStr), blueprintName, hsName, contextStr, + d.Docker, d.BaseImage, d.CSAPIPort, fmt.Sprintf("complement_%s", contextStr), blueprintName, hs.Name, asIDToRegistrationMap, contextStr, networkID, d.config.VersionCheckIterations, ) } // getCaVolume returns the correct mounts and volumes for providing a CA to homeserver containers. -func getCaVolume(docker *client.Client, ctx context.Context) (map[string]struct{}, []mount.Mount, error) { - var caVolume map[string]struct{} - var caMount []mount.Mount +func getCaVolume(docker *client.Client, ctx context.Context) (string, mount.Mount, error) { + var caVolume string + var caMount mount.Mount if os.Getenv("CI") == "true" { // When in CI, Complement itself is a container with the CA volume mounted at /ca. @@ -349,16 +360,16 @@ func getCaVolume(docker *client.Client, ctx context.Context) (map[string]struct{ // /proc/1/cpuset should be /docker/ cpuset, err := ioutil.ReadFile("/proc/1/cpuset") if err != nil { - return nil, nil, err + return caVolume, caMount, err } if !strings.Contains(string(cpuset), "docker") { - return nil, nil, errors.New("Could not identify container ID using /proc/1/cpuset") + return caVolume, caMount, errors.New("Could not identify container ID using /proc/1/cpuset") } cpusetList := strings.Split(strings.TrimSpace(string(cpuset)), "/") containerId := cpusetList[len(cpusetList)-1] container, err := docker.ContainerInspect(ctx, containerId) if err != nil { - return nil, nil, err + return caVolume, caMount, err } // Get the volume that matches the destination in our complement container var volumeName string @@ -371,17 +382,13 @@ func getCaVolume(docker *client.Client, ctx context.Context) (map[string]struct{ // We did not find a volume. This container might be created without a volume, // or CI=true is passed but we are not running in a container. // todo: log that we do not provide a CA volume mount? - return nil, nil, nil + return caVolume, caMount, nil } else { - caVolume = map[string]struct{}{ - "/ca": {}, - } - caMount = []mount.Mount{ - { - Type: mount.TypeVolume, - Source: volumeName, - Target: "/ca", - }, + caVolume = "/ca" + caMount = mount.Mount{ + Type: mount.TypeVolume, + Source: volumeName, + Target: "/ca", } } } else { @@ -389,33 +396,61 @@ func getCaVolume(docker *client.Client, ctx context.Context) (map[string]struct{ // We bind mount this directory to all homeserver containers. cwd, err := os.Getwd() if err != nil { - return nil, nil, err + return caVolume, caMount, err } caCertificateDirHost := path.Join(cwd, "ca") if _, err := os.Stat(caCertificateDirHost); os.IsNotExist(err) { err = os.Mkdir(caCertificateDirHost, 0770) if err != nil { - return nil, nil, err + return caVolume, caMount, err } } - caMount = []mount.Mount{ - { - Type: mount.TypeBind, - Source: path.Join(cwd, "ca"), - Target: "/ca", - }, + + caMount = mount.Mount{ + Type: mount.TypeBind, + Source: path.Join(cwd, "ca"), + Target: "/ca", } } return caVolume, caMount, nil } +func getAppServiceVolume(docker *client.Client, ctx context.Context) (string, mount.Mount, error) { + asVolume, err := docker.VolumeCreate(context.Background(), volume.VolumesCreateBody{ + //Driver: "overlay2", + DriverOpts: map[string]string{}, + Name: "appservices", + }) + + asMount := mount.Mount{ + Type: mount.TypeVolume, + Source: asVolume.Name, + Target: "/appservices", + } + + return "/appservices", asMount, err +} + +func generateASRegistrationYaml(as b.ApplicationService) string { + return fmt.Sprintf("id: %s\n", as.ID) + + fmt.Sprintf("hs_token: %s\n", as.HSToken) + + fmt.Sprintf("as_token: %s\n", as.ASToken) + + fmt.Sprintf("url: '%s'\n", as.URL) + + fmt.Sprintf("sender_localpart: %s\n", as.SenderLocalpart) + + fmt.Sprintf("rate_limited: %v\n", as.RateLimited) + + "namespaces:\n" + + " users: []\n" + + " rooms: []\n" + + " aliases: []\n" +} + func deployImage( - docker *client.Client, imageID string, csPort int, containerName, blueprintName, hsName, contextStr, networkID string, versionCheckIterations int, + docker *client.Client, imageID string, csPort int, containerName, blueprintName, hsName string, asIDToRegistrationMap map[string]string, contextStr, networkID string, versionCheckIterations int, ) (*HomeserverDeployment, error) { ctx := context.Background() var extraHosts []string - var caVolume map[string]struct{} - var caMount []mount.Mount + var volumes = make(map[string]struct{}) + var mounts []mount.Mount var err error if runtime.GOOS == "linux" { @@ -426,26 +461,43 @@ func deployImage( } if os.Getenv("COMPLEMENT_CA") == "true" { + var caVolume string + var caMount mount.Mount caVolume, caMount, err = getCaVolume(docker, ctx) if err != nil { return nil, err } + + volumes[caVolume] = struct{}{} + mounts = append(mounts, caMount) + } + + asVolume, asMount, err := getAppServiceVolume(docker, ctx) + if err != nil { + return nil, err + } + volumes[asVolume] = struct{}{} + mounts = append(mounts, asMount) + + env := []string{ + "SERVER_NAME=" + hsName, + "COMPLEMENT_CA=" + os.Getenv("COMPLEMENT_CA"), } body, err := docker.ContainerCreate(ctx, &container.Config{ Image: imageID, - Env: []string{"SERVER_NAME=" + hsName, "COMPLEMENT_CA=" + os.Getenv("COMPLEMENT_CA")}, + Env: env, //Cmd: d.ImageArgs, Labels: map[string]string{ complementLabel: contextStr, "complement_blueprint": blueprintName, "complement_hs_name": hsName, }, - Volumes: caVolume, + Volumes: volumes, }, &container.HostConfig{ PublishAllPorts: true, ExtraHosts: extraHosts, - Mounts: caMount, + Mounts: mounts, }, &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ hsName: { @@ -457,7 +509,32 @@ func deployImage( if err != nil { return nil, err } + containerID := body.ID + + // Create the application service files + for asID, registration := range asIDToRegistrationMap { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + err = tw.WriteHeader(&tar.Header{ + Name: fmt.Sprintf("/appservices/%s.yaml", asID), + Mode: 0777, + Size: int64(len(registration)), + }) + if err != nil { + return nil, fmt.Errorf("Failed to copy regstration to container: %v", err) + } + tw.Write([]byte(registration)) + tw.Close() + + err = docker.CopyToContainer(context.Background(), containerID, "/", &buf, types.CopyToContainerOptions{ + AllowOverwriteDirWithFile: false, + }) + if err != nil { + return nil, err + } + } + err = docker.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) if err != nil { return nil, err @@ -488,11 +565,13 @@ func deployImage( lastErr = nil break } + d := &HomeserverDeployment{ - BaseURL: baseURL, - FedBaseURL: fedBaseURL, - ContainerID: containerID, - AccessTokens: tokensFromLabels(inspect.Config.Labels), + BaseURL: baseURL, + FedBaseURL: fedBaseURL, + ContainerID: containerID, + AccessTokens: tokensFromLabels(inspect.Config.Labels), + ApplicationServices: asIDToRegistrationFromLabels(inspect.Config.Labels), } if lastErr != nil { return d, fmt.Errorf("%s: failed to check server is up. %w", contextStr, lastErr) @@ -566,6 +645,28 @@ func labelsForTokens(userIDToToken map[string]string) map[string]string { return labels } +func asIDToRegistrationFromLabels(labels map[string]string) map[string]string { + asMap := make(map[string]string) + for k, v := range labels { + if strings.HasPrefix(k, "application_service_") { + asMap[strings.TrimPrefix(k, "application_service_")] = v + } + } + return asMap +} + +func labelsForApplicationServices(hs b.Homeserver) map[string]string { + labels := make(map[string]string) + // collect and store app service registrations as labels 'application_service_$as_id: $registration' + // collect and store app service access tokens as labels 'access_token_$sender_localpart: $as_token' + for _, as := range hs.ApplicationServices { + labels["application_service_"+as.ID] = generateASRegistrationYaml(as) + + labels["access_token_@"+as.SenderLocalpart+":"+hs.Name] = as.ASToken + } + return labels +} + func endpoints(p nat.PortMap, csPort, ssPort int) (baseURL, fedBaseURL string, err error) { csapiPort := fmt.Sprintf("%d/tcp", csPort) csapiPortInfo, ok := p[nat.Port(csapiPort)] diff --git a/internal/docker/deployer.go b/internal/docker/deployer.go index ca473135..b9111848 100644 --- a/internal/docker/deployer.go +++ b/internal/docker/deployer.go @@ -81,10 +81,12 @@ func (d *Deployer) Deploy(ctx context.Context, blueprintName string) (*Deploymen d.Counter++ contextStr := img.Labels["complement_context"] hsName := img.Labels["complement_hs_name"] + asIDToRegistrationMap := asIDToRegistrationFromLabels(img.Labels) + // TODO: Make CSAPI port configurable deployment, err := deployImage( d.Docker, img.ID, 8008, fmt.Sprintf("complement_%s_%s_%d", d.Namespace, contextStr, d.Counter), - blueprintName, hsName, contextStr, networkID, d.config.VersionCheckIterations) + blueprintName, hsName, asIDToRegistrationMap, contextStr, networkID, d.config.VersionCheckIterations) if err != nil { if deployment != nil && deployment.ContainerID != "" { // print logs to help debug diff --git a/internal/docker/deployment.go b/internal/docker/deployment.go index 672a4d59..5aa6c809 100644 --- a/internal/docker/deployment.go +++ b/internal/docker/deployment.go @@ -20,17 +20,23 @@ type Deployment struct { // HomeserverDeployment represents a running homeserver in a container. type HomeserverDeployment struct { - BaseURL string // e.g http://localhost:38646 - FedBaseURL string // e.g https://localhost:48373 - ContainerID string // e.g 10de45efba - AccessTokens map[string]string // e.g { "@alice:hs1": "myAcc3ssT0ken" } + BaseURL string // e.g http://localhost:38646 + FedBaseURL string // e.g https://localhost:48373 + ContainerID string // e.g 10de45efba + AccessTokens map[string]string // e.g { "@alice:hs1": "myAcc3ssT0ken" } + ApplicationServices map[string]string // e.g { "my-as-id": "id: xxx\nas_token: xxx ..."} } } // Destroy the entire deployment. Destroys all running containers. If `printServerLogs` is true, // will print container logs before killing the container. func (d *Deployment) Destroy(t *testing.T) { t.Helper() - d.Deployer.Destroy(d, t.Failed()) + d.Deployer.Destroy( + d, + // TODO: Revert this back to `t.Failed()`. + // I did this so I can always see the homersever logs regardless of outcome + true, + ) } // Client returns a CSAPI client targeting the given hsName, using the access token for the given userID. From ccb20e69cbf177476ed056bf0cf12d43fa7239fc Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Feb 2021 02:10:34 -0600 Subject: [PATCH 2/7] Revert always showing logs --- internal/docker/deployment.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/docker/deployment.go b/internal/docker/deployment.go index 5aa6c809..489b69e2 100644 --- a/internal/docker/deployment.go +++ b/internal/docker/deployment.go @@ -31,12 +31,7 @@ type HomeserverDeployment struct { // will print container logs before killing the container. func (d *Deployment) Destroy(t *testing.T) { t.Helper() - d.Deployer.Destroy( - d, - // TODO: Revert this back to `t.Failed()`. - // I did this so I can always see the homersever logs regardless of outcome - true, - ) + d.Deployer.Destroy(d, t.Failed()) } // Client returns a CSAPI client targeting the given hsName, using the access token for the given userID. From 12e6ec0b8919c8cbb0e950219147522c57f85a72 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Feb 2021 02:12:13 -0600 Subject: [PATCH 3/7] Add comment doc --- internal/docker/builder.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/docker/builder.go b/internal/docker/builder.go index 619ad653..3bec6e35 100644 --- a/internal/docker/builder.go +++ b/internal/docker/builder.go @@ -415,6 +415,8 @@ func getCaVolume(docker *client.Client, ctx context.Context) (string, mount.Moun return caVolume, caMount, nil } +// getAppServiceVolume returns the correct mounts and volumes for providing the `/appservice` directory to homeserver containers +// containing application service registration files to be used by the homeserver func getAppServiceVolume(docker *client.Client, ctx context.Context) (string, mount.Mount, error) { asVolume, err := docker.VolumeCreate(context.Background(), volume.VolumesCreateBody{ //Driver: "overlay2", From 1b19990ca31bfe1a6112fd94e43660ec02dcdc84 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Feb 2021 15:54:47 -0600 Subject: [PATCH 4/7] Some nits and remove the volume paths - Seems like the `Volumes` syntax is to create an anonymous volume, https://stackoverflow.com/a/58916037/796832 - And lots of people not knowing what `Volumes` syntax is or what to do. Seems like Mounts is the thing to use - https://github.com/fsouza/go-dockerclient/issues/155 - https://stackoverflow.com/questions/55718603/golang-docker-library-mounting-host-directory-volumes - https://stackoverflow.com/questions/48470194/defining-a-mount-point-for-volumes-in-golang-docker-sdk --- build/scripts/find-lint.sh | 5 +++- internal/b/blueprints.go | 6 ---- internal/docker/builder.go | 58 ++++++++++++++++---------------------- 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/build/scripts/find-lint.sh b/build/scripts/find-lint.sh index 55f81ed9..a54b8bc2 100755 --- a/build/scripts/find-lint.sh +++ b/build/scripts/find-lint.sh @@ -19,7 +19,10 @@ if [ ${1:-""} = "fast" ] then args="--fast" fi -if [[ -v COMPLEMENT_LINT_CONCURRENCY ]]; then +if [ -z ${COMPLEMENT_LINT_CONCURRENCY+x} ]; then + # COMPLEMENT_LINT_CONCURRENCY was not set + : +else args="${args} --concurrency $COMPLEMENT_LINT_CONCURRENCY" fi diff --git a/internal/b/blueprints.go b/internal/b/blueprints.go index 48223b0b..94cff59d 100644 --- a/internal/b/blueprints.go +++ b/internal/b/blueprints.go @@ -20,8 +20,6 @@ import ( "fmt" "strconv" "strings" - - "github.com/sirupsen/logrus" ) // KnownBlueprints lists static blueprints @@ -133,10 +131,6 @@ func Validate(bp Blueprint) (Blueprint, error) { } } - logrus.WithFields(logrus.Fields{ - "bp": bp.Homeservers[0].ApplicationServices, - }).Error("after modfiying bp") - return bp, nil } diff --git a/internal/docker/builder.go b/internal/docker/builder.go index 3bec6e35..c57bd347 100644 --- a/internal/docker/builder.go +++ b/internal/docker/builder.go @@ -347,29 +347,27 @@ func (d *Builder) deployBaseImage(blueprintName string, hs b.Homeserver, context } // getCaVolume returns the correct mounts and volumes for providing a CA to homeserver containers. -func getCaVolume(docker *client.Client, ctx context.Context) (string, mount.Mount, error) { - var caVolume string - var caMount mount.Mount - +// Returns the +func getCaVolume(ctx context.Context, docker *client.Client) (caMount mount.Mount, err error) { if os.Getenv("CI") == "true" { // When in CI, Complement itself is a container with the CA volume mounted at /ca. // We need to mount this volume to all homeserver containers to synchronize the CA cert. // This is needed to establish trust among all containers. // Get volume mounted at /ca. First we get the container ID - // /proc/1/cpuset should be /docker/ + // /proc/1/cpuset should be /docker/ cpuset, err := ioutil.ReadFile("/proc/1/cpuset") if err != nil { - return caVolume, caMount, err + return caMount, err } if !strings.Contains(string(cpuset), "docker") { - return caVolume, caMount, errors.New("Could not identify container ID using /proc/1/cpuset") + return caMount, errors.New("Could not identify container ID using /proc/1/cpuset") } cpusetList := strings.Split(strings.TrimSpace(string(cpuset)), "/") - containerId := cpusetList[len(cpusetList)-1] - container, err := docker.ContainerInspect(ctx, containerId) + containerID := cpusetList[len(cpusetList)-1] + container, err := docker.ContainerInspect(ctx, containerID) if err != nil { - return caVolume, caMount, err + return caMount, err } // Get the volume that matches the destination in our complement container var volumeName string @@ -382,27 +380,26 @@ func getCaVolume(docker *client.Client, ctx context.Context) (string, mount.Moun // We did not find a volume. This container might be created without a volume, // or CI=true is passed but we are not running in a container. // todo: log that we do not provide a CA volume mount? - return caVolume, caMount, nil - } else { - caVolume = "/ca" - caMount = mount.Mount{ - Type: mount.TypeVolume, - Source: volumeName, - Target: "/ca", - } + return caMount, nil + } + + caMount = mount.Mount{ + Type: mount.TypeVolume, + Source: volumeName, + Target: "/ca", } } else { // When not in CI, our CA cert is placed in the current working dir. // We bind mount this directory to all homeserver containers. cwd, err := os.Getwd() if err != nil { - return caVolume, caMount, err + return caMount, err } caCertificateDirHost := path.Join(cwd, "ca") if _, err := os.Stat(caCertificateDirHost); os.IsNotExist(err) { err = os.Mkdir(caCertificateDirHost, 0770) if err != nil { - return caVolume, caMount, err + return caMount, err } } @@ -412,25 +409,23 @@ func getCaVolume(docker *client.Client, ctx context.Context) (string, mount.Moun Target: "/ca", } } - return caVolume, caMount, nil + return caMount, nil } // getAppServiceVolume returns the correct mounts and volumes for providing the `/appservice` directory to homeserver containers // containing application service registration files to be used by the homeserver -func getAppServiceVolume(docker *client.Client, ctx context.Context) (string, mount.Mount, error) { +func getAppServiceVolume(ctx context.Context, docker *client.Client) (asMount mount.Mount, err error) { asVolume, err := docker.VolumeCreate(context.Background(), volume.VolumesCreateBody{ - //Driver: "overlay2", - DriverOpts: map[string]string{}, - Name: "appservices", + Name: "appservices", }) - asMount := mount.Mount{ + asMount = mount.Mount{ Type: mount.TypeVolume, Source: asVolume.Name, Target: "/appservices", } - return "/appservices", asMount, err + return asMount, err } func generateASRegistrationYaml(as b.ApplicationService) string { @@ -451,7 +446,6 @@ func deployImage( ) (*HomeserverDeployment, error) { ctx := context.Background() var extraHosts []string - var volumes = make(map[string]struct{}) var mounts []mount.Mount var err error @@ -463,22 +457,19 @@ func deployImage( } if os.Getenv("COMPLEMENT_CA") == "true" { - var caVolume string var caMount mount.Mount - caVolume, caMount, err = getCaVolume(docker, ctx) + caMount, err = getCaVolume(ctx, docker) if err != nil { return nil, err } - volumes[caVolume] = struct{}{} mounts = append(mounts, caMount) } - asVolume, asMount, err := getAppServiceVolume(docker, ctx) + asMount, err := getAppServiceVolume(ctx, docker) if err != nil { return nil, err } - volumes[asVolume] = struct{}{} mounts = append(mounts, asMount) env := []string{ @@ -495,7 +486,6 @@ func deployImage( "complement_blueprint": blueprintName, "complement_hs_name": hsName, }, - Volumes: volumes, }, &container.HostConfig{ PublishAllPorts: true, ExtraHosts: extraHosts, From c1f07c280642401a304a8f4bedee59ad5d065c8e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Feb 2021 17:20:55 -0600 Subject: [PATCH 5/7] Address review and add comment docs --- internal/b/blueprints.go | 10 ++++------ internal/docker/builder.go | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/internal/b/blueprints.go b/internal/b/blueprints.go index 94cff59d..b010daae 100644 --- a/internal/b/blueprints.go +++ b/internal/b/blueprints.go @@ -83,12 +83,10 @@ type ApplicationService struct { } type Event struct { - Type string - Sender string - OriginServerTS uint64 - StateKey *string - PrevEvents []string - Content map[string]interface{} + Type string + Sender string + StateKey *string + Content map[string]interface{} // This field is ignored in blueprints as clients are unable to set it. Used with federation.Server Unsigned map[string]interface{} } diff --git a/internal/docker/builder.go b/internal/docker/builder.go index c57bd347..8890f176 100644 --- a/internal/docker/builder.go +++ b/internal/docker/builder.go @@ -39,6 +39,7 @@ import ( "github.com/docker/go-connections/nat" "github.com/matrix-org/complement/internal/b" + internalClient "github.com/matrix-org/complement/internal/client" "github.com/matrix-org/complement/internal/config" "github.com/matrix-org/complement/internal/instruction" ) @@ -346,8 +347,9 @@ func (d *Builder) deployBaseImage(blueprintName string, hs b.Homeserver, context ) } -// getCaVolume returns the correct mounts and volumes for providing a CA to homeserver containers. -// Returns the +// getCaVolume returns the correct volume mount for providing a CA to homeserver containers. +// If running CI, returns an error if it's unable to find a volume that has /ca +// Otherwise, returns an error if we're unable to find the /ca directory on the local host func getCaVolume(ctx context.Context, docker *client.Client) (caMount mount.Mount, err error) { if os.Getenv("CI") == "true" { // When in CI, Complement itself is a container with the CA volume mounted at /ca. @@ -412,12 +414,16 @@ func getCaVolume(ctx context.Context, docker *client.Client) (caMount mount.Moun return caMount, nil } -// getAppServiceVolume returns the correct mounts and volumes for providing the `/appservice` directory to homeserver containers -// containing application service registration files to be used by the homeserver +// getAppServiceVolume returns a volume mount for providing the `/appservice` directory to homeserver containers. +// This directory will contain application service registration config files. +// Returns an error if the volume failed to create func getAppServiceVolume(ctx context.Context, docker *client.Client) (asMount mount.Mount, err error) { asVolume, err := docker.VolumeCreate(context.Background(), volume.VolumesCreateBody{ Name: "appservices", }) + if err != nil { + return asMount, err + } asMount = mount.Mount{ Type: mount.TypeVolume, @@ -506,10 +512,12 @@ func deployImage( // Create the application service files for asID, registration := range asIDToRegistrationMap { + // Create a fake/virtual file in memory that we can copy to the container + // via https://stackoverflow.com/a/52131297/796832 var buf bytes.Buffer tw := tar.NewWriter(&buf) err = tw.WriteHeader(&tar.Header{ - Name: fmt.Sprintf("/appservices/%s.yaml", asID), + Name: fmt.Sprintf("/appservices/%s.yaml", internalClient.GjsonEscape(asID)), Mode: 0777, Size: int64(len(registration)), }) @@ -519,6 +527,7 @@ func deployImage( tw.Write([]byte(registration)) tw.Close() + // Put our new fake file in the container volume err = docker.CopyToContainer(context.Background(), containerID, "/", &buf, types.CopyToContainerOptions{ AllowOverwriteDirWithFile: false, }) From 169a60d510961aa3ce454c1f20e32a08b3430c4e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 22 Feb 2021 17:40:02 -0600 Subject: [PATCH 6/7] Revert lint change already in other PR #73 --- build/scripts/find-lint.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build/scripts/find-lint.sh b/build/scripts/find-lint.sh index a54b8bc2..55f81ed9 100755 --- a/build/scripts/find-lint.sh +++ b/build/scripts/find-lint.sh @@ -19,10 +19,7 @@ if [ ${1:-""} = "fast" ] then args="--fast" fi -if [ -z ${COMPLEMENT_LINT_CONCURRENCY+x} ]; then - # COMPLEMENT_LINT_CONCURRENCY was not set - : -else +if [[ -v COMPLEMENT_LINT_CONCURRENCY ]]; then args="${args} --concurrency $COMPLEMENT_LINT_CONCURRENCY" fi From c6155af04cd029cd3f805de04cf5ca7770924a27 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 23 Feb 2021 16:00:59 +0000 Subject: [PATCH 7/7] Path escape AS IDs to avoid directory traversal attacks --- internal/docker/builder.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/docker/builder.go b/internal/docker/builder.go index 8890f176..4f6fb1bf 100644 --- a/internal/docker/builder.go +++ b/internal/docker/builder.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "os" "path" "runtime" @@ -39,7 +40,6 @@ import ( "github.com/docker/go-connections/nat" "github.com/matrix-org/complement/internal/b" - internalClient "github.com/matrix-org/complement/internal/client" "github.com/matrix-org/complement/internal/config" "github.com/matrix-org/complement/internal/instruction" ) @@ -517,7 +517,7 @@ func deployImage( var buf bytes.Buffer tw := tar.NewWriter(&buf) err = tw.WriteHeader(&tar.Header{ - Name: fmt.Sprintf("/appservices/%s.yaml", internalClient.GjsonEscape(asID)), + Name: fmt.Sprintf("/appservices/%s.yaml", url.PathEscape(asID)), Mode: 0777, Size: int64(len(registration)), })