Merge remote-tracking branch 'upstream/master' into remi/check-roomid-doesnt-exist-when-creating

This commit is contained in:
Remi Reuvekamp 2017-11-20 19:04:55 +01:00
commit fe5029f688
177 changed files with 11108 additions and 1878 deletions

View file

@ -1,6 +1,12 @@
language: go language: go
go: go:
- 1.8 - 1.8
- 1.9
env:
- TEST_SUITE="lint"
- TEST_SUITE="unit-test"
- TEST_SUITE="integ-test"
sudo: false sudo: false
@ -8,9 +14,6 @@ sudo: false
dist: trusty dist: trusty
addons: addons:
apt:
packages:
- openssl
postgresql: "9.5" postgresql: "9.5"
services: services:
@ -19,12 +22,7 @@ services:
install: install:
- go get github.com/constabulary/gb/... - go get github.com/constabulary/gb/...
# Generate a self-signed X.509 certificate for TLS.
before_script:
- openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj /CN=localhost
script: script:
- ./scripts/install-local-kafka.sh
- ./scripts/travis-test.sh - ./scripts/travis-test.sh
notifications: notifications:

View file

@ -15,6 +15,10 @@ for that line or statement using a [comment directive](https://github.com/alecth
`// nolint: gocyclo`. This should be used sparingly and only when its clear `// nolint: gocyclo`. This should be used sparingly and only when its clear
that the lint warning is spurious. that the lint warning is spurious.
The linters are vendored, and can be run using [scripts/find-lint.sh](scripts/find-lint.sh)
(see file for docs) or as part of a build/test/lint cycle using
[scripts/build-test-lint.sh](scripts/build-test-lint.sh).
## HTTP Error Handling ## HTTP Error Handling

View file

@ -13,7 +13,8 @@ instance of dendrite, and [CODE_STYLE.md](CODE_STYLE.md) for the code style
guide. guide.
We use `gb` for managing our dependencies, so `gb build` and `gb test` is how We use `gb` for managing our dependencies, so `gb build` and `gb test` is how
to build dendrite and run the unit tests respectively. to build dendrite and run the unit tests respectively. There are [scripts](scripts)
for [linting](scripts/find-lint.sh) and doing a [build/test/lint run](scripts/build-test-lint.sh).
## Picking Things To Do ## Picking Things To Do

View file

@ -54,6 +54,24 @@ media:
height: 600 height: 600
method: scale method: scale
# The config for the TURN server
turn:
# Whether or not guests can request TURN credentials
turn_allow_guests: true
# How long the authorization should last
turn_user_lifetime: "1h"
# The list of TURN URIs to pass to clients
turn_uris: []
# Authorization via Shared Secret
# The shared secret from coturn
turn_shared_secret: "<SECRET STRING GOES HERE>"
# Authorization via Static Username & Password
# Hardcoded Username and Password
turn_username: ""
turn_password: ""
# The config for communicating with kafka # The config for communicating with kafka
kafka: kafka:
# Where the kafka servers are running. # Where the kafka servers are running.

View file

@ -2,4 +2,4 @@
set -eu set -eu
./scripts/build-test-lint.sh ./scripts/find-lint.sh fast

View file

@ -4,7 +4,6 @@
"Deadline": "5m", "Deadline": "5m",
"Enable": [ "Enable": [
"vetshadow", "vetshadow",
"gotype",
"deadcode", "deadcode",
"gocyclo", "gocyclo",
"ineffassign", "ineffassign",
@ -12,6 +11,7 @@
"misspell", "misspell",
"errcheck", "errcheck",
"vet", "vet",
"gofmt",
"goconst" "goconst"
] ]
} }

View file

@ -4,13 +4,12 @@
"Deadline": "5m", "Deadline": "5m",
"Enable": [ "Enable": [
"vetshadow", "vetshadow",
"gotype",
"deadcode", "deadcode",
"gocyclo", "gocyclo",
"golint", "golint",
"varcheck", "varcheck",
"structcheck", "structcheck",
"aligncheck", "maligned",
"ineffassign", "ineffassign",
"gas", "gas",
"misspell", "misspell",
@ -18,6 +17,7 @@
"errcheck", "errcheck",
"vet", "vet",
"megacheck", "megacheck",
"gofmt",
"goconst" "goconst"
] ]
} }

View file

@ -5,7 +5,7 @@
set -eu set -eu
export GOPATH="$(pwd):$(pwd)/vendor" export GOPATH="$(pwd):$(pwd)/vendor"
export PATH="$PATH:$(pwd)/vendor/bin:$(pwd)/bin" export PATH="$PATH:$(pwd)/bin"
echo "Checking that it builds" echo "Checking that it builds"
gb build gb build
@ -19,8 +19,5 @@ go build github.com/matrix-org/dendrite/cmd/...
./scripts/find-lint.sh ./scripts/find-lint.sh
echo "Double checking spelling..."
misspell -error src *.md
echo "Testing..." echo "Testing..."
gb test gb test

View file

@ -14,7 +14,7 @@
set -eu set -eu
export GOPATH="$(pwd):$(pwd)/vendor" export GOPATH="$(pwd):$(pwd)/vendor"
export PATH="$PATH:$(pwd)/vendor/bin:$(pwd)/bin" export PATH="$PATH:$(pwd)/bin"
args="" args=""
if [ ${1:-""} = "fast" ] if [ ${1:-""} = "fast" ]
@ -31,7 +31,7 @@ then args="$args --enable-gc"
fi fi
echo "Installing lint search engine..." echo "Installing lint search engine..."
go install github.com/alecthomas/gometalinter/ gb build github.com/alecthomas/gometalinter/
gometalinter --config=linter.json ./... --install gometalinter --config=linter.json ./... --install
echo "Looking for lint..." echo "Looking for lint..."

View file

@ -1,6 +1,10 @@
#! /bin/bash #! /bin/bash
# The entry point for travis tests # The entry point for travis tests
#
# TEST_SUITE env var can be set to "lint", "unit-test" or "integ-test", in
# which case only the linting, unit tests or integration tests will be run
# respectively. If not specified or null all tests are run.
set -eu set -eu
@ -8,20 +12,44 @@ set -eu
export GOGC=400 export GOGC=400
export DENDRITE_LINT_DISABLE_GC=1 export DENDRITE_LINT_DISABLE_GC=1
# Check that the servers build (this is done explicitly because `gb build` can silently fail (exit 0) and then we'd test a stale binary) export GOPATH="$(pwd):$(pwd)/vendor"
gb build github.com/matrix-org/dendrite/cmd/dendrite-room-server export PATH="$PATH:$(pwd)/bin"
gb build github.com/matrix-org/dendrite/cmd/roomserver-integration-tests
gb build github.com/matrix-org/dendrite/cmd/dendrite-sync-api-server
gb build github.com/matrix-org/dendrite/cmd/syncserver-integration-tests
gb build github.com/matrix-org/dendrite/cmd/create-account
gb build github.com/matrix-org/dendrite/cmd/dendrite-media-api-server
gb build github.com/matrix-org/dendrite/cmd/mediaapi-integration-tests
gb build github.com/matrix-org/dendrite/cmd/client-api-proxy
# Run unit tests and linters if [ "${TEST_SUITE:-lint}" == "lint" ]; then
./scripts/build-test-lint.sh ./scripts/find-lint.sh
fi
# Run the integration tests if [ "${TEST_SUITE:-unit-test}" == "unit-test" ]; then
bin/roomserver-integration-tests gb test
bin/syncserver-integration-tests fi
bin/mediaapi-integration-tests
if [ "${TEST_SUITE:-integ-test}" == "integ-test" ]; then
gb build
# Check that all the packages can build.
# When `go build` is given multiple packages it won't output anything, and just
# checks that everything builds. This seems to do a better job of handling
# missing imports than `gb build` does.
go build github.com/matrix-org/dendrite/cmd/...
# Check that the servers build (this is done explicitly because `gb build` can silently fail (exit 0) and then we'd test a stale binary)
gb build github.com/matrix-org/dendrite/cmd/dendrite-room-server
gb build github.com/matrix-org/dendrite/cmd/roomserver-integration-tests
gb build github.com/matrix-org/dendrite/cmd/dendrite-sync-api-server
gb build github.com/matrix-org/dendrite/cmd/syncserver-integration-tests
gb build github.com/matrix-org/dendrite/cmd/create-account
gb build github.com/matrix-org/dendrite/cmd/dendrite-media-api-server
gb build github.com/matrix-org/dendrite/cmd/mediaapi-integration-tests
gb build github.com/matrix-org/dendrite/cmd/client-api-proxy
# Create necessary certificates and keys to run dendrite
echo "Generating certs..."
time openssl req -x509 -newkey rsa:512 -keyout server.key -out server.crt -days 365 -nodes -subj /CN=localhost
echo "Installing kafka..."
time ./scripts/install-local-kafka.sh
# Run the integration tests
bin/roomserver-integration-tests
bin/syncserver-integration-tests
bin/mediaapi-integration-tests
fi

View file

@ -25,6 +25,7 @@ import (
"strings" "strings"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
@ -62,10 +63,8 @@ func VerifyAccessToken(req *http.Request, deviceDB DeviceDatabase) (device *auth
JSON: jsonerror.UnknownToken("Unknown token"), JSON: jsonerror.UnknownToken("Unknown token"),
} }
} else { } else {
resErr = &util.JSONResponse{ jsonErr := httputil.LogThenError(req, err)
Code: 500, resErr = &jsonErr
JSON: jsonerror.Unknown("Failed to check access token"),
}
} }
} }
return return

View file

@ -17,6 +17,8 @@ package accounts
import ( import (
"context" "context"
"database/sql" "database/sql"
"github.com/matrix-org/gomatrixserverlib"
) )
const filterSchema = ` const filterSchema = `
@ -38,12 +40,16 @@ CREATE INDEX IF NOT EXISTS account_filter_localpart ON account_filter(localpart)
const selectFilterSQL = "" + const selectFilterSQL = "" +
"SELECT filter FROM account_filter WHERE localpart = $1 AND id = $2" "SELECT filter FROM account_filter WHERE localpart = $1 AND id = $2"
const selectFilterIDByContentSQL = "" +
"SELECT id FROM account_filter WHERE localpart = $1 AND filter = $2"
const insertFilterSQL = "" + const insertFilterSQL = "" +
"INSERT INTO account_filter (filter, id, localpart) VALUES ($1, DEFAULT, $2) RETURNING id" "INSERT INTO account_filter (filter, id, localpart) VALUES ($1, DEFAULT, $2) RETURNING id"
type filterStatements struct { type filterStatements struct {
selectFilterStmt *sql.Stmt selectFilterStmt *sql.Stmt
insertFilterStmt *sql.Stmt selectFilterIDByContentStmt *sql.Stmt
insertFilterStmt *sql.Stmt
} }
func (s *filterStatements) prepare(db *sql.DB) (err error) { func (s *filterStatements) prepare(db *sql.DB) (err error) {
@ -54,6 +60,9 @@ func (s *filterStatements) prepare(db *sql.DB) (err error) {
if s.selectFilterStmt, err = db.Prepare(selectFilterSQL); err != nil { if s.selectFilterStmt, err = db.Prepare(selectFilterSQL); err != nil {
return return
} }
if s.selectFilterIDByContentStmt, err = db.Prepare(selectFilterIDByContentSQL); err != nil {
return
}
if s.insertFilterStmt, err = db.Prepare(insertFilterSQL); err != nil { if s.insertFilterStmt, err = db.Prepare(insertFilterSQL); err != nil {
return return
} }
@ -62,14 +71,37 @@ func (s *filterStatements) prepare(db *sql.DB) (err error) {
func (s *filterStatements) selectFilter( func (s *filterStatements) selectFilter(
ctx context.Context, localpart string, filterID string, ctx context.Context, localpart string, filterID string,
) (filter string, err error) { ) (filter []byte, err error) {
err = s.selectFilterStmt.QueryRowContext(ctx, localpart, filterID).Scan(&filter) err = s.selectFilterStmt.QueryRowContext(ctx, localpart, filterID).Scan(&filter)
return return
} }
func (s *filterStatements) insertFilter( func (s *filterStatements) insertFilter(
ctx context.Context, filter string, localpart string, ctx context.Context, filter []byte, localpart string,
) (pos string, err error) { ) (filterID string, err error) {
err = s.insertFilterStmt.QueryRowContext(ctx, filter, localpart).Scan(&pos) var existingFilterID string
// This can result in a race condition when two clients try to insert the
// same filter and localpart at the same time, however this is not a
// problem as both calls will result in the same filterID
filterJSON, err := gomatrixserverlib.CanonicalJSON(filter)
if err != nil {
return "", err
}
// Check if filter already exists in the database
err = s.selectFilterIDByContentStmt.QueryRowContext(ctx,
localpart, filterJSON).Scan(&existingFilterID)
if err != nil && err != sql.ErrNoRows {
return "", err
}
// If it does, return the existing ID
if existingFilterID != "" {
return existingFilterID, err
}
// Otherwise insert the filter and return the new ID
err = s.insertFilterStmt.QueryRowContext(ctx, filterJSON, localpart).
Scan(&filterID)
return return
} }

View file

@ -321,22 +321,25 @@ func (d *Database) GetThreePIDsForLocalpart(
} }
// GetFilter looks up the filter associated with a given local user and filter ID. // GetFilter looks up the filter associated with a given local user and filter ID.
// Returns an error if no such filter exists or if there was an error taling to the database. // Returns a filter represented as a byte slice. Otherwise returns an error if
// no such filter exists or if there was an error talking to the database.
func (d *Database) GetFilter( func (d *Database) GetFilter(
ctx context.Context, localpart string, filterID string, ctx context.Context, localpart string, filterID string,
) (string, error) { ) ([]byte, error) {
return d.filter.selectFilter(ctx, localpart, filterID) return d.filter.selectFilter(ctx, localpart, filterID)
} }
// PutFilter puts the passed filter into the database. // PutFilter puts the passed filter into the database.
// Returns an error if something goes wrong. // Returns the filterID as a string. Otherwise returns an error if something
// goes wrong.
func (d *Database) PutFilter( func (d *Database) PutFilter(
ctx context.Context, localpart, filter string, ctx context.Context, localpart string, filter []byte,
) (string, error) { ) (string, error) {
return d.filter.insertFilter(ctx, filter, localpart) return d.filter.insertFilter(ctx, filter, localpart)
} }
// CheckAccountAvailability checks if the username/localpart is already present in the database. // CheckAccountAvailability checks if the username/localpart is already present
// in the database.
// If the DB returns sql.ErrNoRows the Localpart isn't taken. // If the DB returns sql.ErrNoRows the Localpart isn't taken.
func (d *Database) CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) { func (d *Database) CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) {
_, err := d.accounts.selectAccountByLocalpart(ctx, localpart) _, err := d.accounts.selectAccountByLocalpart(ctx, localpart)

View file

@ -40,7 +40,9 @@ CREATE TABLE IF NOT EXISTS device_devices (
-- migration to different domain names easier. -- migration to different domain names easier.
localpart TEXT NOT NULL, localpart TEXT NOT NULL,
-- When this devices was first recognised on the network, as a unix timestamp (ms resolution). -- When this devices was first recognised on the network, as a unix timestamp (ms resolution).
created_ts BIGINT NOT NULL created_ts BIGINT NOT NULL,
-- The display name, human friendlier than device_id and updatable
display_name TEXT
-- TODO: device keys, device display names, last used ts and IP address?, token restrictions (if 3rd-party OAuth app) -- TODO: device keys, device display names, last used ts and IP address?, token restrictions (if 3rd-party OAuth app)
); );
@ -49,21 +51,35 @@ CREATE UNIQUE INDEX IF NOT EXISTS device_localpart_id_idx ON device_devices(loca
` `
const insertDeviceSQL = "" + const insertDeviceSQL = "" +
"INSERT INTO device_devices(device_id, localpart, access_token, created_ts) VALUES ($1, $2, $3, $4)" "INSERT INTO device_devices(device_id, localpart, access_token, created_ts, display_name) VALUES ($1, $2, $3, $4, $5)"
const selectDeviceByTokenSQL = "" + const selectDeviceByTokenSQL = "" +
"SELECT device_id, localpart FROM device_devices WHERE access_token = $1" "SELECT device_id, localpart FROM device_devices WHERE access_token = $1"
const selectDeviceByIDSQL = "" +
"SELECT display_name FROM device_devices WHERE localpart = $1 and device_id = $2"
const selectDevicesByLocalpartSQL = "" +
"SELECT device_id, display_name FROM device_devices WHERE localpart = $1"
const updateDeviceNameSQL = "" +
"UPDATE device_devices SET display_name = $1 WHERE localpart = $2 AND device_id = $3"
const deleteDeviceSQL = "" + const deleteDeviceSQL = "" +
"DELETE FROM device_devices WHERE device_id = $1 AND localpart = $2" "DELETE FROM device_devices WHERE device_id = $1 AND localpart = $2"
// TODO: List devices? const deleteDevicesByLocalpartSQL = "" +
"DELETE FROM device_devices WHERE localpart = $1"
type devicesStatements struct { type devicesStatements struct {
insertDeviceStmt *sql.Stmt insertDeviceStmt *sql.Stmt
selectDeviceByTokenStmt *sql.Stmt selectDeviceByTokenStmt *sql.Stmt
deleteDeviceStmt *sql.Stmt selectDeviceByIDStmt *sql.Stmt
serverName gomatrixserverlib.ServerName selectDevicesByLocalpartStmt *sql.Stmt
updateDeviceNameStmt *sql.Stmt
deleteDeviceStmt *sql.Stmt
deleteDevicesByLocalpartStmt *sql.Stmt
serverName gomatrixserverlib.ServerName
} }
func (s *devicesStatements) prepare(db *sql.DB, server gomatrixserverlib.ServerName) (err error) { func (s *devicesStatements) prepare(db *sql.DB, server gomatrixserverlib.ServerName) (err error) {
@ -77,9 +93,21 @@ func (s *devicesStatements) prepare(db *sql.DB, server gomatrixserverlib.ServerN
if s.selectDeviceByTokenStmt, err = db.Prepare(selectDeviceByTokenSQL); err != nil { if s.selectDeviceByTokenStmt, err = db.Prepare(selectDeviceByTokenSQL); err != nil {
return return
} }
if s.selectDeviceByIDStmt, err = db.Prepare(selectDeviceByIDSQL); err != nil {
return
}
if s.selectDevicesByLocalpartStmt, err = db.Prepare(selectDevicesByLocalpartSQL); err != nil {
return
}
if s.updateDeviceNameStmt, err = db.Prepare(updateDeviceNameSQL); err != nil {
return
}
if s.deleteDeviceStmt, err = db.Prepare(deleteDeviceSQL); err != nil { if s.deleteDeviceStmt, err = db.Prepare(deleteDeviceSQL); err != nil {
return return
} }
if s.deleteDevicesByLocalpartStmt, err = db.Prepare(deleteDevicesByLocalpartSQL); err != nil {
return
}
s.serverName = server s.serverName = server
return return
} }
@ -89,10 +117,11 @@ func (s *devicesStatements) prepare(db *sql.DB, server gomatrixserverlib.ServerN
// Returns the device on success. // Returns the device on success.
func (s *devicesStatements) insertDevice( func (s *devicesStatements) insertDevice(
ctx context.Context, txn *sql.Tx, id, localpart, accessToken string, ctx context.Context, txn *sql.Tx, id, localpart, accessToken string,
displayName *string,
) (*authtypes.Device, error) { ) (*authtypes.Device, error) {
createdTimeMS := time.Now().UnixNano() / 1000000 createdTimeMS := time.Now().UnixNano() / 1000000
stmt := common.TxStmt(txn, s.insertDeviceStmt) stmt := common.TxStmt(txn, s.insertDeviceStmt)
if _, err := stmt.ExecContext(ctx, id, localpart, accessToken, createdTimeMS); err != nil { if _, err := stmt.ExecContext(ctx, id, localpart, accessToken, createdTimeMS, displayName); err != nil {
return nil, err return nil, err
} }
return &authtypes.Device{ return &authtypes.Device{
@ -110,6 +139,22 @@ func (s *devicesStatements) deleteDevice(
return err return err
} }
func (s *devicesStatements) deleteDevicesByLocalpart(
ctx context.Context, txn *sql.Tx, localpart string,
) error {
stmt := common.TxStmt(txn, s.deleteDevicesByLocalpartStmt)
_, err := stmt.ExecContext(ctx, localpart)
return err
}
func (s *devicesStatements) updateDeviceName(
ctx context.Context, txn *sql.Tx, localpart, deviceID string, displayName *string,
) error {
stmt := common.TxStmt(txn, s.updateDeviceNameStmt)
_, err := stmt.ExecContext(ctx, displayName, localpart, deviceID)
return err
}
func (s *devicesStatements) selectDeviceByToken( func (s *devicesStatements) selectDeviceByToken(
ctx context.Context, accessToken string, ctx context.Context, accessToken string,
) (*authtypes.Device, error) { ) (*authtypes.Device, error) {
@ -124,6 +169,44 @@ func (s *devicesStatements) selectDeviceByToken(
return &dev, err return &dev, err
} }
func (s *devicesStatements) selectDeviceByID(
ctx context.Context, localpart, deviceID string,
) (*authtypes.Device, error) {
var dev authtypes.Device
var created int64
stmt := s.selectDeviceByIDStmt
err := stmt.QueryRowContext(ctx, localpart, deviceID).Scan(&created)
if err == nil {
dev.ID = deviceID
dev.UserID = makeUserID(localpart, s.serverName)
}
return &dev, err
}
func (s *devicesStatements) selectDevicesByLocalpart(
ctx context.Context, localpart string,
) ([]authtypes.Device, error) {
devices := []authtypes.Device{}
rows, err := s.selectDevicesByLocalpartStmt.QueryContext(ctx, localpart)
if err != nil {
return devices, err
}
for rows.Next() {
var dev authtypes.Device
err = rows.Scan(&dev.ID)
if err != nil {
return devices, err
}
dev.UserID = makeUserID(localpart, s.serverName)
devices = append(devices, dev)
}
return devices, nil
}
func makeUserID(localpart string, server gomatrixserverlib.ServerName) string { func makeUserID(localpart string, server gomatrixserverlib.ServerName) string {
return fmt.Sprintf("@%s:%s", localpart, string(server)) return fmt.Sprintf("@%s:%s", localpart, string(server))
} }

View file

@ -52,6 +52,21 @@ func (d *Database) GetDeviceByAccessToken(
return d.devices.selectDeviceByToken(ctx, token) return d.devices.selectDeviceByToken(ctx, token)
} }
// GetDeviceByID returns the device matching the given ID.
// Returns sql.ErrNoRows if no matching device was found.
func (d *Database) GetDeviceByID(
ctx context.Context, localpart, deviceID string,
) (*authtypes.Device, error) {
return d.devices.selectDeviceByID(ctx, localpart, deviceID)
}
// GetDevicesByLocalpart returns the devices matching the given localpart.
func (d *Database) GetDevicesByLocalpart(
ctx context.Context, localpart string,
) ([]authtypes.Device, error) {
return d.devices.selectDevicesByLocalpart(ctx, localpart)
}
// CreateDevice makes a new device associated with the given user ID localpart. // CreateDevice makes a new device associated with the given user ID localpart.
// If there is already a device with the same device ID for this user, that access token will be revoked // If there is already a device with the same device ID for this user, that access token will be revoked
// and replaced with the given accessToken. If the given accessToken is already in use for another device, // and replaced with the given accessToken. If the given accessToken is already in use for another device,
@ -60,6 +75,7 @@ func (d *Database) GetDeviceByAccessToken(
// Returns the device on success. // Returns the device on success.
func (d *Database) CreateDevice( func (d *Database) CreateDevice(
ctx context.Context, localpart string, deviceID *string, accessToken string, ctx context.Context, localpart string, deviceID *string, accessToken string,
displayName *string,
) (dev *authtypes.Device, returnErr error) { ) (dev *authtypes.Device, returnErr error) {
if deviceID != nil { if deviceID != nil {
returnErr = common.WithTransaction(d.db, func(txn *sql.Tx) error { returnErr = common.WithTransaction(d.db, func(txn *sql.Tx) error {
@ -69,7 +85,7 @@ func (d *Database) CreateDevice(
return err return err
} }
dev, err = d.devices.insertDevice(ctx, txn, *deviceID, localpart, accessToken) dev, err = d.devices.insertDevice(ctx, txn, *deviceID, localpart, accessToken, displayName)
return err return err
}) })
} else { } else {
@ -84,7 +100,7 @@ func (d *Database) CreateDevice(
returnErr = common.WithTransaction(d.db, func(txn *sql.Tx) error { returnErr = common.WithTransaction(d.db, func(txn *sql.Tx) error {
var err error var err error
dev, err = d.devices.insertDevice(ctx, txn, newDeviceID, localpart, accessToken) dev, err = d.devices.insertDevice(ctx, txn, newDeviceID, localpart, accessToken, displayName)
return err return err
}) })
if returnErr == nil { if returnErr == nil {
@ -95,6 +111,16 @@ func (d *Database) CreateDevice(
return return
} }
// UpdateDevice updates the given device with the display name.
// Returns SQL error if there are problems and nil on success.
func (d *Database) UpdateDevice(
ctx context.Context, localpart, deviceID string, displayName *string,
) error {
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
return d.devices.updateDeviceName(ctx, txn, localpart, deviceID, displayName)
})
}
// RemoveDevice revokes a device by deleting the entry in the database // RemoveDevice revokes a device by deleting the entry in the database
// matching with the given device ID and user ID localpart // matching with the given device ID and user ID localpart
// If the device doesn't exist, it will not return an error // If the device doesn't exist, it will not return an error
@ -109,3 +135,17 @@ func (d *Database) RemoveDevice(
return nil return nil
}) })
} }
// RemoveAllDevices revokes devices by deleting the entry in the
// database matching the given user ID localpart.
// If something went wrong during the deletion, it will return the SQL error.
func (d *Database) RemoveAllDevices(
ctx context.Context, localpart string,
) error {
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
if err := d.devices.deleteDevicesByLocalpart(ctx, txn, localpart); err != sql.ErrNoRows {
return err
}
return nil
})
}

View file

@ -96,11 +96,7 @@ func (s *OutputRoomEventConsumer) onMessage(msg *sarama.ConsumerMessage) error {
return err return err
} }
if err := s.db.UpdateMemberships(context.TODO(), events, output.NewRoomEvent.RemovesStateEventIDs); err != nil { return s.db.UpdateMemberships(context.TODO(), events, output.NewRoomEvent.RemovesStateEventIDs)
return err
}
return nil
} }
// lookupStateEvents looks up the state events that are added by a new event. // lookupStateEvents looks up the state events that are added by a new event.

View file

@ -67,6 +67,18 @@ func NotFound(msg string) *MatrixError {
return &MatrixError{"M_NOT_FOUND", msg} return &MatrixError{"M_NOT_FOUND", msg}
} }
// MissingArgument is an error when the client tries to access a resource
// without providing an argument that is required.
func MissingArgument(msg string) *MatrixError {
return &MatrixError{"M_MISSING_ARGUMENT", msg}
}
// InvalidArgumentValue is an error when the client tries to provide an
// invalid value for a valid argument
func InvalidArgumentValue(msg string) *MatrixError {
return &MatrixError{"M_INVALID_ARGUMENT_VALUE", msg}
}
// MissingToken is an error when the client tries to access a resource which // MissingToken is an error when the client tries to access a resource which
// requires authentication without supplying credentials. // requires authentication without supplying credentials.
func MissingToken(msg string) *MatrixError { func MissingToken(msg string) *MatrixError {

View file

@ -0,0 +1,155 @@
// Copyright 2017 Paul Tötterman <paul.totterman@iki.fi>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"database/sql"
"encoding/json"
"net/http"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
type deviceJSON struct {
DeviceID string `json:"device_id"`
UserID string `json:"user_id"`
}
type devicesJSON struct {
Devices []deviceJSON `json:"devices"`
}
type deviceUpdateJSON struct {
DisplayName *string `json:"display_name"`
}
// GetDeviceByID handles /device/{deviceID}
func GetDeviceByID(
req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
deviceID string,
) util.JSONResponse {
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
return httputil.LogThenError(req, err)
}
ctx := req.Context()
dev, err := deviceDB.GetDeviceByID(ctx, localpart, deviceID)
if err == sql.ErrNoRows {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound("Unknown device"),
}
} else if err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: deviceJSON{
DeviceID: dev.ID,
UserID: dev.UserID,
},
}
}
// GetDevicesByLocalpart handles /devices
func GetDevicesByLocalpart(
req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
) util.JSONResponse {
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
return httputil.LogThenError(req, err)
}
ctx := req.Context()
deviceList, err := deviceDB.GetDevicesByLocalpart(ctx, localpart)
if err != nil {
return httputil.LogThenError(req, err)
}
res := devicesJSON{}
for _, dev := range deviceList {
res.Devices = append(res.Devices, deviceJSON{
DeviceID: dev.ID,
UserID: dev.UserID,
})
}
return util.JSONResponse{
Code: 200,
JSON: res,
}
}
// UpdateDeviceByID handles PUT on /devices/{deviceID}
func UpdateDeviceByID(
req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
deviceID string,
) util.JSONResponse {
if req.Method != "PUT" {
return util.JSONResponse{
Code: 405,
JSON: jsonerror.NotFound("Bad Method"),
}
}
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
return httputil.LogThenError(req, err)
}
ctx := req.Context()
dev, err := deviceDB.GetDeviceByID(ctx, localpart, deviceID)
if err == sql.ErrNoRows {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound("Unknown device"),
}
} else if err != nil {
return httputil.LogThenError(req, err)
}
if dev.UserID != device.UserID {
return util.JSONResponse{
Code: 403,
JSON: jsonerror.Forbidden("device not owned by current user"),
}
}
defer req.Body.Close() // nolint: errcheck
payload := deviceUpdateJSON{}
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
return httputil.LogThenError(req, err)
}
if err := deviceDB.UpdateDevice(ctx, localpart, deviceID, payload.DisplayName); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}

View file

@ -60,7 +60,7 @@ func GetFilter(
} }
} }
filter := gomatrix.Filter{} filter := gomatrix.Filter{}
err = json.Unmarshal([]byte(res), &filter) err = json.Unmarshal(res, &filter)
if err != nil { if err != nil {
httputil.LogThenError(req, err) httputil.LogThenError(req, err)
} }
@ -111,7 +111,7 @@ func PutFilter(
} }
} }
filterID, err := accountDB.PutFilter(req.Context(), localpart, string(filterArray)) filterID, err := accountDB.PutFilter(req.Context(), localpart, filterArray)
if err != nil { if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }

View file

@ -22,10 +22,10 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/events"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrix" "github.com/matrix-org/gomatrix"
@ -215,7 +215,7 @@ func (r joinRoomReq) joinRoomUsingServers(
} }
var queryRes api.QueryLatestEventsAndStateResponse var queryRes api.QueryLatestEventsAndStateResponse
event, err := events.BuildEvent(r.req.Context(), &eb, r.cfg, r.queryAPI, &queryRes) event, err := common.BuildEvent(r.req.Context(), &eb, r.cfg, r.queryAPI, &queryRes)
if err == nil { if err == nil {
if err = r.producer.SendEvents(r.req.Context(), []gomatrixserverlib.Event{*event}, r.cfg.Matrix.ServerName); err != nil { if err = r.producer.SendEvents(r.req.Context(), []gomatrixserverlib.Event{*event}, r.cfg.Matrix.ServerName); err != nil {
return httputil.LogThenError(r.req, err) return httputil.LogThenError(r.req, err)
@ -227,7 +227,7 @@ func (r joinRoomReq) joinRoomUsingServers(
}{roomID}, }{roomID},
} }
} }
if err != events.ErrRoomNoExists { if err != common.ErrRoomNoExists {
return httputil.LogThenError(r.req, err) return httputil.LogThenError(r.req, err)
} }

View file

@ -38,8 +38,9 @@ type flow struct {
} }
type passwordRequest struct { type passwordRequest struct {
User string `json:"user"` User string `json:"user"`
Password string `json:"password"` Password string `json:"password"`
InitialDisplayName *string `json:"initial_device_display_name"`
} }
type loginResponse struct { type loginResponse struct {
@ -119,7 +120,7 @@ func Login(
// TODO: Use the device ID in the request // TODO: Use the device ID in the request
dev, err := deviceDB.CreateDevice( dev, err := deviceDB.CreateDevice(
req.Context(), acc.Localpart, nil, token, req.Context(), acc.Localpart, nil, token, r.InitialDisplayName,
) )
if err != nil { if err != nil {
return util.JSONResponse{ return util.JSONResponse{

View file

@ -50,3 +50,22 @@ func Logout(
JSON: struct{}{}, JSON: struct{}{},
} }
} }
// LogoutAll handles POST /logout/all
func LogoutAll(
req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
) util.JSONResponse {
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
return httputil.LogThenError(req, err)
}
if err := deviceDB.RemoveAllDevices(req.Context(), localpart); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}

View file

@ -21,7 +21,6 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/events"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
@ -62,7 +61,7 @@ func SendMembership(
Code: 400, Code: 400,
JSON: jsonerror.NotTrusted(body.IDServer), JSON: jsonerror.NotTrusted(body.IDServer),
} }
} else if err == events.ErrRoomNoExists { } else if err == common.ErrRoomNoExists {
return util.JSONResponse{ return util.JSONResponse{
Code: 404, Code: 404,
JSON: jsonerror.NotFound(err.Error()), JSON: jsonerror.NotFound(err.Error()),
@ -89,7 +88,7 @@ func SendMembership(
Code: 400, Code: 400,
JSON: jsonerror.BadJSON(err.Error()), JSON: jsonerror.BadJSON(err.Error()),
} }
} else if err == events.ErrRoomNoExists { } else if err == common.ErrRoomNoExists {
return util.JSONResponse{ return util.JSONResponse{
Code: 404, Code: 404,
JSON: jsonerror.NotFound(err.Error()), JSON: jsonerror.NotFound(err.Error()),
@ -149,7 +148,7 @@ func buildMembershipEvent(
return nil, err return nil, err
} }
return events.BuildEvent(ctx, &builder, cfg, queryAPI, nil) return common.BuildEvent(ctx, &builder, cfg, queryAPI, nil)
} }
// loadProfile lookups the profile of a given user from the database and returns // loadProfile lookups the profile of a given user from the database and returns

View file

@ -33,7 +33,7 @@ type response struct {
// GetMemberships implements GET /rooms/{roomId}/members // GetMemberships implements GET /rooms/{roomId}/members
func GetMemberships( func GetMemberships(
req *http.Request, device *authtypes.Device, roomID string, joinedOnly bool, req *http.Request, device *authtypes.Device, roomID string, joinedOnly bool,
cfg config.Dendrite, _ config.Dendrite,
queryAPI api.RoomserverQueryAPI, queryAPI api.RoomserverQueryAPI,
) util.JSONResponse { ) util.JSONResponse {
queryReq := api.QueryMembershipsForRoomRequest{ queryReq := api.QueryMembershipsForRoomRequest{

View file

@ -20,7 +20,6 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/events"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
@ -32,19 +31,6 @@ import (
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
type profileResponse struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"displayname"`
}
type avatarURL struct {
AvatarURL string `json:"avatar_url"`
}
type displayName struct {
DisplayName string `json:"displayname"`
}
// GetProfile implements GET /profile/{userID} // GetProfile implements GET /profile/{userID}
func GetProfile( func GetProfile(
req *http.Request, accountDB *accounts.Database, userID string, req *http.Request, accountDB *accounts.Database, userID string,
@ -64,7 +50,7 @@ func GetProfile(
if err != nil { if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
res := profileResponse{ res := common.ProfileResponse{
AvatarURL: profile.AvatarURL, AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName, DisplayName: profile.DisplayName,
} }
@ -87,7 +73,7 @@ func GetAvatarURL(
if err != nil { if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
res := avatarURL{ res := common.AvatarURL{
AvatarURL: profile.AvatarURL, AvatarURL: profile.AvatarURL,
} }
return util.JSONResponse{ return util.JSONResponse{
@ -111,7 +97,7 @@ func SetAvatarURL(
changedKey := "avatar_url" changedKey := "avatar_url"
var r avatarURL var r common.AvatarURL
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil { if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr return *resErr
} }
@ -179,7 +165,7 @@ func GetDisplayName(
if err != nil { if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
res := displayName{ res := common.DisplayName{
DisplayName: profile.DisplayName, DisplayName: profile.DisplayName,
} }
return util.JSONResponse{ return util.JSONResponse{
@ -203,7 +189,7 @@ func SetDisplayName(
changedKey := "displayname" changedKey := "displayname"
var r displayName var r common.DisplayName
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil { if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr return *resErr
} }
@ -285,7 +271,7 @@ func buildMembershipEvents(
return nil, err return nil, err
} }
event, err := events.BuildEvent(ctx, &builder, *cfg, queryAPI, nil) event, err := common.BuildEvent(ctx, &builder, *cfg, queryAPI, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -28,7 +28,6 @@ import (
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
@ -37,6 +36,7 @@ import (
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
log "github.com/sirupsen/logrus"
) )
const ( const (
@ -60,6 +60,8 @@ type registerRequest struct {
Admin bool `json:"admin"` Admin bool `json:"admin"`
// user-interactive auth params // user-interactive auth params
Auth authDict `json:"auth"` Auth authDict `json:"auth"`
InitialDisplayName *string `json:"initial_device_display_name"`
} }
type authDict struct { type authDict struct {
@ -210,10 +212,10 @@ func Register(
return util.MessageResponse(403, "HMAC incorrect") return util.MessageResponse(403, "HMAC incorrect")
} }
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, r.InitialDisplayName)
case authtypes.LoginTypeDummy: case authtypes.LoginTypeDummy:
// there is nothing to do // there is nothing to do
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, r.InitialDisplayName)
default: default:
return util.JSONResponse{ return util.JSONResponse{
Code: 501, Code: 501,
@ -270,10 +272,10 @@ func LegacyRegister(
return util.MessageResponse(403, "HMAC incorrect") return util.MessageResponse(403, "HMAC incorrect")
} }
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, nil)
case authtypes.LoginTypeDummy: case authtypes.LoginTypeDummy:
// there is nothing to do // there is nothing to do
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, nil)
default: default:
return util.JSONResponse{ return util.JSONResponse{
Code: 501, Code: 501,
@ -287,6 +289,7 @@ func completeRegistration(
accountDB *accounts.Database, accountDB *accounts.Database,
deviceDB *devices.Database, deviceDB *devices.Database,
username, password string, username, password string,
displayName *string,
) util.JSONResponse { ) util.JSONResponse {
if username == "" { if username == "" {
return util.JSONResponse{ return util.JSONResponse{
@ -318,7 +321,7 @@ func completeRegistration(
} }
// // TODO: Use the device ID in the request. // // TODO: Use the device ID in the request.
dev, err := deviceDB.CreateDevice(ctx, username, nil, token) dev, err := deviceDB.CreateDevice(ctx, username, nil, token, displayName)
if err != nil { if err != nil {
return util.JSONResponse{ return util.JSONResponse{
Code: 500, Code: 500,

View file

@ -160,6 +160,12 @@ func Setup(
}), }),
).Methods("POST", "OPTIONS") ).Methods("POST", "OPTIONS")
r0mux.Handle("/logout/all",
common.MakeAuthAPI("logout", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
return LogoutAll(req, deviceDB, device)
}),
).Methods("POST", "OPTIONS")
// Stub endpoints required by Riot // Stub endpoints required by Riot
r0mux.Handle("/login", r0mux.Handle("/login",
@ -278,12 +284,8 @@ func Setup(
).Methods("PUT", "OPTIONS") ).Methods("PUT", "OPTIONS")
r0mux.Handle("/voip/turnServer", r0mux.Handle("/voip/turnServer",
common.MakeExternalAPI("turn_server", func(req *http.Request) util.JSONResponse { common.MakeAuthAPI("turn_server", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
// TODO: Return credentials for a turn server if one is configured. return RequestTurnServer(req, device, cfg)
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}), }),
).Methods("GET") ).Methods("GET")
@ -349,6 +351,26 @@ func Setup(
}), }),
).Methods("PUT", "OPTIONS") ).Methods("PUT", "OPTIONS")
r0mux.Handle("/devices",
common.MakeAuthAPI("get_devices", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
return GetDevicesByLocalpart(req, deviceDB, device)
}),
).Methods("GET")
r0mux.Handle("/device/{deviceID}",
common.MakeAuthAPI("get_device", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req)
return GetDeviceByID(req, deviceDB, device, vars["deviceID"])
}),
).Methods("GET")
r0mux.Handle("/devices/{deviceID}",
common.MakeAuthAPI("device_data", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req)
return UpdateDeviceByID(req, deviceDB, device, vars["deviceID"])
}),
).Methods("PUT", "OPTIONS")
// Stub implementations for sytest // Stub implementations for sytest
r0mux.Handle("/events", r0mux.Handle("/events",
common.MakeExternalAPI("events", func(req *http.Request) util.JSONResponse { common.MakeExternalAPI("events", func(req *http.Request) util.JSONResponse {

View file

@ -18,10 +18,10 @@ import (
"net/http" "net/http"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/events"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
@ -67,8 +67,8 @@ func SendEvent(
} }
var queryRes api.QueryLatestEventsAndStateResponse var queryRes api.QueryLatestEventsAndStateResponse
e, err := events.BuildEvent(req.Context(), &builder, cfg, queryAPI, &queryRes) e, err := common.BuildEvent(req.Context(), &builder, cfg, queryAPI, &queryRes)
if err == events.ErrRoomNoExists { if err == common.ErrRoomNoExists {
return util.JSONResponse{ return util.JSONResponse{
Code: 404, Code: 404,
JSON: jsonerror.NotFound("Room does not exist"), JSON: jsonerror.NotFound("Room does not exist"),

View file

@ -0,0 +1,79 @@
// Copyright 2017 Michael Telatysnki <7t3chguy@gmail.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"net/http"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"time"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/gomatrix"
"github.com/matrix-org/util"
)
// RequestTurnServer implements:
// GET /voip/turnServer
func RequestTurnServer(req *http.Request, device *authtypes.Device, cfg config.Dendrite) util.JSONResponse {
turnConfig := cfg.TURN
// TODO Guest Support
if len(turnConfig.URIs) == 0 || turnConfig.UserLifetime == "" {
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
// Duration checked at startup, err not possible
duration, _ := time.ParseDuration(turnConfig.UserLifetime)
resp := gomatrix.RespTurnServer{
URIs: turnConfig.URIs,
TTL: int(duration.Seconds()),
}
if turnConfig.SharedSecret != "" {
expiry := time.Now().Add(duration).Unix()
mac := hmac.New(sha1.New, []byte(turnConfig.SharedSecret))
_, err := mac.Write([]byte(resp.Username))
if err != nil {
return httputil.LogThenError(req, err)
}
resp.Username = fmt.Sprintf("%d:%s", expiry, device.UserID)
resp.Password = base64.StdEncoding.EncodeToString(mac.Sum(nil))
} else if turnConfig.Username != "" && turnConfig.Password != "" {
resp.Username = turnConfig.Username
resp.Password = turnConfig.Password
} else {
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
return util.JSONResponse{
Code: 200,
JSON: resp,
}
}

View file

@ -26,7 +26,6 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/events"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
@ -351,14 +350,10 @@ func emit3PIDInviteEvent(
} }
var queryRes *api.QueryLatestEventsAndStateResponse var queryRes *api.QueryLatestEventsAndStateResponse
event, err := events.BuildEvent(ctx, builder, cfg, queryAPI, queryRes) event, err := common.BuildEvent(ctx, builder, cfg, queryAPI, queryRes)
if err != nil { if err != nil {
return err return err
} }
if err := producer.SendEvents(ctx, []gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil { return producer.SendEvents(ctx, []gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName)
return err
}
return nil
} }

View file

@ -87,7 +87,7 @@ func main() {
} }
device, err := deviceDB.CreateDevice( device, err := deviceDB.CreateDevice(
context.Background(), *username, nil, *accessToken, context.Background(), *username, nil, *accessToken, nil,
) )
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())

View file

@ -98,13 +98,7 @@ func main() {
log.Panicf("Failed to setup key database(%q): %s", cfg.Database.ServerKey, err.Error()) log.Panicf("Failed to setup key database(%q): %s", cfg.Database.ServerKey, err.Error())
} }
keyRing := gomatrixserverlib.KeyRing{ keyRing := keydb.CreateKeyRing(federation.Client, keyDB)
KeyFetchers: []gomatrixserverlib.KeyFetcher{
// TODO: Use perspective key fetchers for production.
&gomatrixserverlib.DirectKeyFetcher{Client: federation.Client},
},
KeyDatabase: keyDB,
}
kafkaConsumer, err := sarama.NewConsumer(cfg.Kafka.Addresses, nil) kafkaConsumer, err := sarama.NewConsumer(cfg.Kafka.Addresses, nil)
if err != nil { if err != nil {

View file

@ -80,6 +80,7 @@ func main() {
queryAPI := api.NewRoomserverQueryAPIHTTP(cfg.RoomServerURL(), nil) queryAPI := api.NewRoomserverQueryAPIHTTP(cfg.RoomServerURL(), nil)
inputAPI := api.NewRoomserverInputAPIHTTP(cfg.RoomServerURL(), nil) inputAPI := api.NewRoomserverInputAPIHTTP(cfg.RoomServerURL(), nil)
aliasAPI := api.NewRoomserverAliasAPIHTTP(cfg.RoomServerURL(), nil)
roomserverProducer := producers.NewRoomserverProducer(inputAPI) roomserverProducer := producers.NewRoomserverProducer(inputAPI)
@ -90,7 +91,7 @@ func main() {
log.Info("Starting federation API server on ", cfg.Listen.FederationAPI) log.Info("Starting federation API server on ", cfg.Listen.FederationAPI)
api := mux.NewRouter() api := mux.NewRouter()
routing.Setup(api, *cfg, queryAPI, roomserverProducer, keyRing, federation, accountDB) routing.Setup(api, *cfg, queryAPI, aliasAPI, roomserverProducer, keyRing, federation, accountDB)
common.SetupHTTPAPI(http.DefaultServeMux, api) common.SetupHTTPAPI(http.DefaultServeMux, api)
log.Fatal(http.ListenAndServe(string(cfg.Listen.FederationAPI), nil)) log.Fatal(http.ListenAndServe(string(cfg.Listen.FederationAPI), nil))

View file

@ -16,6 +16,7 @@ package main
import ( import (
"context" "context"
"database/sql"
"flag" "flag"
"net/http" "net/http"
"os" "os"
@ -194,18 +195,26 @@ func (m *monolith) setupFederation() {
m.cfg.Matrix.ServerName, m.cfg.Matrix.KeyID, m.cfg.Matrix.PrivateKey, m.cfg.Matrix.ServerName, m.cfg.Matrix.KeyID, m.cfg.Matrix.PrivateKey,
) )
m.keyRing = gomatrixserverlib.KeyRing{ m.keyRing = keydb.CreateKeyRing(m.federation.Client, m.keyDB)
KeyFetchers: []gomatrixserverlib.KeyFetcher{
// TODO: Use perspective key fetchers for production.
&gomatrixserverlib.DirectKeyFetcher{Client: m.federation.Client},
},
KeyDatabase: m.keyDB,
}
} }
func (m *monolith) setupKafka() { func (m *monolith) setupKafka() {
if m.cfg.Kafka.UseNaffka { if m.cfg.Kafka.UseNaffka {
naff, err := naffka.New(&naffka.MemoryDatabase{}) db, err := sql.Open("postgres", string(m.cfg.Database.Naffka))
if err != nil {
log.WithFields(log.Fields{
log.ErrorKey: err,
}).Panic("Failed to open naffka database")
}
naffkaDB, err := naffka.NewPostgresqlDatabase(db)
if err != nil {
log.WithFields(log.Fields{
log.ErrorKey: err,
}).Panic("Failed to setup naffka database")
}
naff, err := naffka.New(naffkaDB)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
log.ErrorKey: err, log.ErrorKey: err,
@ -336,10 +345,10 @@ func (m *monolith) setupAPIs() {
syncapi_routing.Setup(m.api, syncapi_sync.NewRequestPool( syncapi_routing.Setup(m.api, syncapi_sync.NewRequestPool(
m.syncAPIDB, m.syncAPINotifier, m.accountDB, m.syncAPIDB, m.syncAPINotifier, m.accountDB,
), m.deviceDB) ), m.syncAPIDB, m.deviceDB)
federationapi_routing.Setup( federationapi_routing.Setup(
m.api, *m.cfg, m.queryAPI, m.roomServerProducer, m.keyRing, m.federation, m.api, *m.cfg, m.queryAPI, m.aliasAPI, m.roomServerProducer, m.keyRing, m.federation,
m.accountDB, m.accountDB,
) )

View file

@ -104,7 +104,7 @@ func main() {
log.Info("Starting sync server on ", cfg.Listen.SyncAPI) log.Info("Starting sync server on ", cfg.Listen.SyncAPI)
api := mux.NewRouter() api := mux.NewRouter()
routing.Setup(api, sync.NewRequestPool(db, n, adb), deviceDB) routing.Setup(api, sync.NewRequestPool(db, n, adb), db, deviceDB)
common.SetupHTTPAPI(http.DefaultServeMux, api) common.SetupHTTPAPI(http.DefaultServeMux, api)
log.Fatal(http.ListenAndServe(string(cfg.Listen.SyncAPI), nil)) log.Fatal(http.ListenAndServe(string(cfg.Listen.SyncAPI), nil))

View file

@ -75,7 +75,7 @@ var timeout time.Duration
var port = 10000 var port = 10000
func startMediaAPI(suffix string, dynamicThumbnails bool) (*exec.Cmd, chan error, string, *exec.Cmd, chan error, string, string) { func startMediaAPI(suffix string, dynamicThumbnails bool) (*exec.Cmd, chan error, *exec.Cmd, string, string) {
dir, err := ioutil.TempDir("", serverType+"-server-test"+suffix) dir, err := ioutil.TempDir("", serverType+"-server-test"+suffix)
if err != nil { if err != nil {
panic(err) panic(err)
@ -107,7 +107,7 @@ func startMediaAPI(suffix string, dynamicThumbnails bool) (*exec.Cmd, chan error
testDatabaseName + suffix, testDatabaseName + suffix,
} }
proxyCmd, proxyCmdChan := test.StartProxy(proxyAddr, cfg) proxyCmd, _ := test.StartProxy(proxyAddr, cfg)
test.InitDatabase( test.InitDatabase(
postgresDatabase, postgresDatabase,
@ -121,7 +121,7 @@ func startMediaAPI(suffix string, dynamicThumbnails bool) (*exec.Cmd, chan error
) )
fmt.Printf("==TESTSERVER== STARTED %v -> %v : %v\n", proxyAddr, cfg.Listen.MediaAPI, dir) fmt.Printf("==TESTSERVER== STARTED %v -> %v : %v\n", proxyAddr, cfg.Listen.MediaAPI, dir)
return cmd, cmdChan, string(cfg.Listen.MediaAPI), proxyCmd, proxyCmdChan, proxyAddr, dir return cmd, cmdChan, proxyCmd, proxyAddr, dir
} }
func cleanUpServer(cmd *exec.Cmd, dir string) { func cleanUpServer(cmd *exec.Cmd, dir string) {
@ -145,7 +145,7 @@ func main() {
} }
// create server1 with only pre-generated thumbnails allowed // create server1 with only pre-generated thumbnails allowed
server1Cmd, server1CmdChan, _, server1ProxyCmd, _, server1ProxyAddr, server1Dir := startMediaAPI("1", false) server1Cmd, server1CmdChan, server1ProxyCmd, server1ProxyAddr, server1Dir := startMediaAPI("1", false)
defer cleanUpServer(server1Cmd, server1Dir) defer cleanUpServer(server1Cmd, server1Dir)
defer server1ProxyCmd.Process.Kill() // nolint: errcheck defer server1ProxyCmd.Process.Kill() // nolint: errcheck
testDownload(server1ProxyAddr, server1ProxyAddr, "doesnotexist", 404, server1CmdChan) testDownload(server1ProxyAddr, server1ProxyAddr, "doesnotexist", 404, server1CmdChan)
@ -162,7 +162,7 @@ func main() {
testThumbnail(64, 64, "crop", server1ProxyAddr, server1CmdChan) testThumbnail(64, 64, "crop", server1ProxyAddr, server1CmdChan)
// create server2 with dynamic thumbnail generation // create server2 with dynamic thumbnail generation
server2Cmd, server2CmdChan, _, server2ProxyCmd, _, server2ProxyAddr, server2Dir := startMediaAPI("2", true) server2Cmd, server2CmdChan, server2ProxyCmd, server2ProxyAddr, server2Dir := startMediaAPI("2", true)
defer cleanUpServer(server2Cmd, server2Dir) defer cleanUpServer(server2Cmd, server2Dir)
defer server2ProxyCmd.Process.Kill() // nolint: errcheck defer server2ProxyCmd.Process.Kill() // nolint: errcheck
testDownload(server2ProxyAddr, server2ProxyAddr, "doesnotexist", 404, server2CmdChan) testDownload(server2ProxyAddr, server2ProxyAddr, "doesnotexist", 404, server2CmdChan)

View file

@ -25,8 +25,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -148,8 +148,30 @@ type Dendrite struct {
// The PublicRoomsAPI database stores information used to compute the public // The PublicRoomsAPI database stores information used to compute the public
// room directory. It is only accessed by the PublicRoomsAPI server. // room directory. It is only accessed by the PublicRoomsAPI server.
PublicRoomsAPI DataSource `yaml:"public_rooms_api"` PublicRoomsAPI DataSource `yaml:"public_rooms_api"`
// The Naffka database is used internally by the naffka library, if used.
Naffka DataSource `yaml:"naffka,omitempty"`
} `yaml:"database"` } `yaml:"database"`
// TURN Server Config
TURN struct {
// TODO Guest Support
// Whether or not guests can request TURN credentials
//AllowGuests bool `yaml:"turn_allow_guests"`
// How long the authorization should last
UserLifetime string `yaml:"turn_user_lifetime"`
// The list of TURN URIs to pass to clients
URIs []string `yaml:"turn_uris"`
// Authorization via Shared Secret
// The shared secret from coturn
SharedSecret string `yaml:"turn_shared_secret"`
// Authorization via Static Username & Password
// Hardcoded Username and Password
Username string `yaml:"turn_username"`
Password string `yaml:"turn_password"`
}
// The internal addresses the components will listen on. // The internal addresses the components will listen on.
// These should not be exposed externally as they expose metrics and debugging APIs. // These should not be exposed externally as they expose metrics and debugging APIs.
Listen struct { Listen struct {
@ -341,10 +363,20 @@ func (config *Dendrite) check(monolithic bool) error {
} }
} }
checkValidDuration := func(key, value string) {
if _, err := time.ParseDuration(config.TURN.UserLifetime); err != nil {
problems = append(problems, fmt.Sprintf("invalid duration for config key %q: %s", key, value))
}
}
checkNotEmpty("matrix.server_name", string(config.Matrix.ServerName)) checkNotEmpty("matrix.server_name", string(config.Matrix.ServerName))
checkNotEmpty("matrix.private_key", string(config.Matrix.PrivateKeyPath)) checkNotEmpty("matrix.private_key", string(config.Matrix.PrivateKeyPath))
checkNotZero("matrix.federation_certificates", int64(len(config.Matrix.FederationCertificatePaths))) checkNotZero("matrix.federation_certificates", int64(len(config.Matrix.FederationCertificatePaths)))
if config.TURN.UserLifetime != "" {
checkValidDuration("turn.turn_user_lifetime", config.TURN.UserLifetime)
}
checkNotEmpty("media.base_path", string(config.Media.BasePath)) checkNotEmpty("media.base_path", string(config.Media.BasePath))
checkPositive("media.max_file_size_bytes", int64(*config.Media.MaxFileSizeBytes)) checkPositive("media.max_file_size_bytes", int64(*config.Media.MaxFileSizeBytes))
checkPositive("media.max_thumbnail_generators", int64(config.Media.MaxThumbnailGenerators)) checkPositive("media.max_thumbnail_generators", int64(config.Media.MaxThumbnailGenerators))
@ -356,6 +388,8 @@ func (config *Dendrite) check(monolithic bool) error {
if !monolithic { if !monolithic {
problems = append(problems, fmt.Sprintf("naffka can only be used in a monolithic server")) problems = append(problems, fmt.Sprintf("naffka can only be used in a monolithic server"))
} }
checkNotEmpty("database.naffka", string(config.Database.Naffka))
} else { } else {
// If we aren't using naffka then we need to have at least one kafka // If we aren't using naffka then we need to have at least one kafka
// server to talk to. // server to talk to.

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package events package common
import ( import (
"context" "context"

View file

@ -48,14 +48,14 @@ func NewDatabase(dataSourceName string) (*Database, error) {
func (d *Database) FetchKeys( func (d *Database) FetchKeys(
ctx context.Context, ctx context.Context,
requests map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.Timestamp, requests map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.Timestamp,
) (map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.ServerKeys, error) { ) (map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.PublicKeyLookupResult, error) {
return d.statements.bulkSelectServerKeys(ctx, requests) return d.statements.bulkSelectServerKeys(ctx, requests)
} }
// StoreKeys implements gomatrixserverlib.KeyDatabase // StoreKeys implements gomatrixserverlib.KeyDatabase
func (d *Database) StoreKeys( func (d *Database) StoreKeys(
ctx context.Context, ctx context.Context,
keyMap map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.ServerKeys, keyMap map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.PublicKeyLookupResult,
) error { ) error {
// TODO: Inserting all the keys within a single transaction may // TODO: Inserting all the keys within a single transaction may
// be more efficient since the transaction overhead can be quite // be more efficient since the transaction overhead can be quite

View file

@ -0,0 +1,32 @@
// Copyright 2017 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package keydb
import "github.com/matrix-org/gomatrixserverlib"
// CreateKeyRing creates and configures a KeyRing object.
//
// It creates the necessary key fetchers and collects them into a KeyRing
// backed by the given KeyDatabase.
func CreateKeyRing(client gomatrixserverlib.Client,
keyDB gomatrixserverlib.KeyDatabase) gomatrixserverlib.KeyRing {
return gomatrixserverlib.KeyRing{
KeyFetchers: []gomatrixserverlib.KeyFetcher{
// TODO: Use perspective key fetchers for production.
&gomatrixserverlib.DirectKeyFetcher{Client: client},
},
KeyDatabase: keyDB,
}
}

View file

@ -17,14 +17,13 @@ package keydb
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
) )
const serverKeysSchema = ` const serverKeysSchema = `
-- A cache of server keys downloaded from remote servers. -- A cache of signing keys downloaded from remote servers.
CREATE TABLE IF NOT EXISTS keydb_server_keys ( CREATE TABLE IF NOT EXISTS keydb_server_keys (
-- The name of the matrix server the key is for. -- The name of the matrix server the key is for.
server_name TEXT NOT NULL, server_name TEXT NOT NULL,
@ -33,10 +32,14 @@ CREATE TABLE IF NOT EXISTS keydb_server_keys (
-- Combined server name and key ID separated by the ASCII unit separator -- Combined server name and key ID separated by the ASCII unit separator
-- to make it easier to run bulk queries. -- to make it easier to run bulk queries.
server_name_and_key_id TEXT NOT NULL, server_name_and_key_id TEXT NOT NULL,
-- When the keys are valid until as a millisecond timestamp. -- When the key is valid until as a millisecond timestamp.
-- 0 if this is an expired key (in which case expired_ts will be non-zero)
valid_until_ts BIGINT NOT NULL, valid_until_ts BIGINT NOT NULL,
-- The raw JSON for the server key. -- When the key expired as a millisecond timestamp.
server_key_json TEXT NOT NULL, -- 0 if this is an active key (in which case valid_until_ts will be non-zero)
expired_ts BIGINT NOT NULL,
-- The base64-encoded public key.
server_key TEXT NOT NULL,
CONSTRAINT keydb_server_keys_unique UNIQUE (server_name, server_key_id) CONSTRAINT keydb_server_keys_unique UNIQUE (server_name, server_key_id)
); );
@ -44,15 +47,16 @@ CREATE INDEX IF NOT EXISTS keydb_server_name_and_key_id ON keydb_server_keys (se
` `
const bulkSelectServerKeysSQL = "" + const bulkSelectServerKeysSQL = "" +
"SELECT server_name, server_key_id, server_key_json FROM keydb_server_keys" + "SELECT server_name, server_key_id, valid_until_ts, expired_ts, " +
" server_key FROM keydb_server_keys" +
" WHERE server_name_and_key_id = ANY($1)" " WHERE server_name_and_key_id = ANY($1)"
const upsertServerKeysSQL = "" + const upsertServerKeysSQL = "" +
"INSERT INTO keydb_server_keys (server_name, server_key_id," + "INSERT INTO keydb_server_keys (server_name, server_key_id," +
" server_name_and_key_id, valid_until_ts, server_key_json)" + " server_name_and_key_id, valid_until_ts, expired_ts, server_key)" +
" VALUES ($1, $2, $3, $4, $5)" + " VALUES ($1, $2, $3, $4, $5, $6)" +
" ON CONFLICT ON CONSTRAINT keydb_server_keys_unique" + " ON CONFLICT ON CONSTRAINT keydb_server_keys_unique" +
" DO UPDATE SET valid_until_ts = $4, server_key_json = $5" " DO UPDATE SET valid_until_ts = $4, expired_ts = $5, server_key = $6"
type serverKeyStatements struct { type serverKeyStatements struct {
bulkSelectServerKeysStmt *sql.Stmt bulkSelectServerKeysStmt *sql.Stmt
@ -76,7 +80,7 @@ func (s *serverKeyStatements) prepare(db *sql.DB) (err error) {
func (s *serverKeyStatements) bulkSelectServerKeys( func (s *serverKeyStatements) bulkSelectServerKeys(
ctx context.Context, ctx context.Context,
requests map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.Timestamp, requests map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.Timestamp,
) (map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.ServerKeys, error) { ) (map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.PublicKeyLookupResult, error) {
var nameAndKeyIDs []string var nameAndKeyIDs []string
for request := range requests { for request := range requests {
nameAndKeyIDs = append(nameAndKeyIDs, nameAndKeyID(request)) nameAndKeyIDs = append(nameAndKeyIDs, nameAndKeyID(request))
@ -87,23 +91,30 @@ func (s *serverKeyStatements) bulkSelectServerKeys(
return nil, err return nil, err
} }
defer rows.Close() // nolint: errcheck defer rows.Close() // nolint: errcheck
results := map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.ServerKeys{} results := map[gomatrixserverlib.PublicKeyRequest]gomatrixserverlib.PublicKeyLookupResult{}
for rows.Next() { for rows.Next() {
var serverName string var serverName string
var keyID string var keyID string
var keyJSON []byte var key string
if err := rows.Scan(&serverName, &keyID, &keyJSON); err != nil { var validUntilTS int64
return nil, err var expiredTS int64
} if err = rows.Scan(&serverName, &keyID, &validUntilTS, &expiredTS, &key); err != nil {
var serverKeys gomatrixserverlib.ServerKeys
if err := json.Unmarshal(keyJSON, &serverKeys); err != nil {
return nil, err return nil, err
} }
r := gomatrixserverlib.PublicKeyRequest{ r := gomatrixserverlib.PublicKeyRequest{
ServerName: gomatrixserverlib.ServerName(serverName), ServerName: gomatrixserverlib.ServerName(serverName),
KeyID: gomatrixserverlib.KeyID(keyID), KeyID: gomatrixserverlib.KeyID(keyID),
} }
results[r] = serverKeys vk := gomatrixserverlib.VerifyKey{}
err = vk.Key.Decode(key)
if err != nil {
return nil, err
}
results[r] = gomatrixserverlib.PublicKeyLookupResult{
VerifyKey: vk,
ValidUntilTS: gomatrixserverlib.Timestamp(validUntilTS),
ExpiredTS: gomatrixserverlib.Timestamp(expiredTS),
}
} }
return results, nil return results, nil
} }
@ -111,19 +122,16 @@ func (s *serverKeyStatements) bulkSelectServerKeys(
func (s *serverKeyStatements) upsertServerKeys( func (s *serverKeyStatements) upsertServerKeys(
ctx context.Context, ctx context.Context,
request gomatrixserverlib.PublicKeyRequest, request gomatrixserverlib.PublicKeyRequest,
keys gomatrixserverlib.ServerKeys, key gomatrixserverlib.PublicKeyLookupResult,
) error { ) error {
keyJSON, err := json.Marshal(keys) _, err := s.upsertServerKeysStmt.ExecContext(
if err != nil {
return err
}
_, err = s.upsertServerKeysStmt.ExecContext(
ctx, ctx,
string(request.ServerName), string(request.ServerName),
string(request.KeyID), string(request.KeyID),
nameAndKeyID(request), nameAndKeyID(request),
int64(keys.ValidUntilTS), key.ValidUntilTS,
keyJSON, key.ExpiredTS,
key.Key.Encode(),
) )
return err return err
} }

View file

@ -18,8 +18,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/sirupsen/logrus"
"github.com/matrix-org/dugong" "github.com/matrix-org/dugong"
"github.com/sirupsen/logrus"
) )
type utcFormatter struct { type utcFormatter struct {

View file

@ -113,10 +113,7 @@ func WriteConfig(cfg *config.Dendrite, configDir string) error {
if err != nil { if err != nil {
return err return err
} }
if err = ioutil.WriteFile(filepath.Join(configDir, ConfigFile), data, 0666); err != nil { return ioutil.WriteFile(filepath.Join(configDir, ConfigFile), data, 0666)
return err
}
return nil
} }
// NewMatrixKey generates a new ed25519 matrix server key and writes it to a file. // NewMatrixKey generates a new ed25519 matrix server key and writes it to a file.

View file

@ -20,3 +20,19 @@ type AccountData struct {
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
Type string `json:"type"` Type string `json:"type"`
} }
// ProfileResponse is a struct containing all known user profile data
type ProfileResponse struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"displayname"`
}
// AvatarURL is a struct containing only the URL to a user's avatar
type AvatarURL struct {
AvatarURL string `json:"avatar_url"`
}
// DisplayName is a struct containing only a user's display name
type DisplayName struct {
DisplayName string `json:"displayname"`
}

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package readers package routing
import ( import (
"context" "context"
@ -28,10 +28,10 @@ import (
func GetEvent( func GetEvent(
ctx context.Context, ctx context.Context,
request *gomatrixserverlib.FederationRequest, request *gomatrixserverlib.FederationRequest,
cfg config.Dendrite, _ config.Dendrite,
query api.RoomserverQueryAPI, query api.RoomserverQueryAPI,
now time.Time, _ time.Time,
keys gomatrixserverlib.KeyRing, _ gomatrixserverlib.KeyRing,
eventID string, eventID string,
) util.JSONResponse { ) util.JSONResponse {
var authResponse api.QueryServerAllowedToSeeEventResponse var authResponse api.QueryServerAllowedToSeeEventResponse

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package readers package routing
import ( import (
"encoding/json" "encoding/json"
@ -38,7 +38,6 @@ func localKeys(cfg config.Dendrite, validUntil time.Time) (*gomatrixserverlib.Se
var keys gomatrixserverlib.ServerKeys var keys gomatrixserverlib.ServerKeys
keys.ServerName = cfg.Matrix.ServerName keys.ServerName = cfg.Matrix.ServerName
keys.FromServer = cfg.Matrix.ServerName
publicKey := cfg.Matrix.PrivateKey.Public().(ed25519.PublicKey) publicKey := cfg.Matrix.PrivateKey.Public().(ed25519.PublicKey)

View file

@ -0,0 +1,87 @@
// Copyright 2017 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"net/http"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
// GetProfile implements GET /_matrix/federation/v1/query/profile
func GetProfile(
httpReq *http.Request,
accountDB *accounts.Database,
cfg config.Dendrite,
) util.JSONResponse {
userID, field := httpReq.FormValue("user_id"), httpReq.FormValue("field")
// httpReq.FormValue will return an empty string if value is not found
if userID == "" {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.MissingArgument("The request body did not contain required argument 'user_id'."),
}
}
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return httputil.LogThenError(httpReq, err)
}
if domain != cfg.Matrix.ServerName {
return httputil.LogThenError(httpReq, err)
}
profile, err := accountDB.GetProfileByLocalpart(httpReq.Context(), localpart)
if err != nil {
return httputil.LogThenError(httpReq, err)
}
var res interface{}
code := 200
if field != "" {
switch field {
case "displayname":
res = common.DisplayName{
DisplayName: profile.DisplayName,
}
case "avatar_url":
res = common.AvatarURL{
AvatarURL: profile.AvatarURL,
}
default:
code = 400
res = jsonerror.InvalidArgumentValue("The request body did not contain an allowed value of argument 'field'. Allowed values are either: 'avatar_url', 'displayname'.")
}
} else {
res = common.ProfileResponse{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
}
}
return util.JSONResponse{
Code: code,
JSON: res,
}
}

View file

@ -0,0 +1,96 @@
// Copyright 2017 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"fmt"
"net/http"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrix"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
// RoomAliasToID converts the queried alias into a room ID and returns it
func RoomAliasToID(
httpReq *http.Request,
federation *gomatrixserverlib.FederationClient,
cfg config.Dendrite,
aliasAPI api.RoomserverAliasAPI,
) util.JSONResponse {
roomAlias := httpReq.FormValue("alias")
if roomAlias == "" {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON("Must supply room alias parameter."),
}
}
_, domain, err := gomatrixserverlib.SplitID('#', roomAlias)
if err != nil {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
}
}
var resp gomatrixserverlib.RespDirectory
if domain == cfg.Matrix.ServerName {
queryReq := api.GetAliasRoomIDRequest{Alias: roomAlias}
var queryRes api.GetAliasRoomIDResponse
if err = aliasAPI.GetAliasRoomID(httpReq.Context(), &queryReq, &queryRes); err != nil {
return httputil.LogThenError(httpReq, err)
}
if queryRes.RoomID == "" {
// TODO: List servers that are aware of this room alias
resp = gomatrixserverlib.RespDirectory{
RoomID: queryRes.RoomID,
Servers: []gomatrixserverlib.ServerName{},
}
} else {
// If the response doesn't contain a non-empty string, return an error
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound(fmt.Sprintf("Room alias %s not found", roomAlias)),
}
}
} else {
resp, err = federation.LookupRoomAlias(httpReq.Context(), domain, roomAlias)
if err != nil {
switch x := err.(type) {
case gomatrix.HTTPError:
if x.Code == 404 {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound("Room alias not found"),
}
}
}
// TODO: Return 502 if the remote server errored.
// TODO: Return 504 if the remote server timed out.
return httputil.LogThenError(httpReq, err)
}
}
return util.JSONResponse{
Code: 200,
JSON: resp,
}
}

View file

@ -23,7 +23,6 @@ import (
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/federationapi/readers"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
@ -39,6 +38,7 @@ func Setup(
apiMux *mux.Router, apiMux *mux.Router,
cfg config.Dendrite, cfg config.Dendrite,
query api.RoomserverQueryAPI, query api.RoomserverQueryAPI,
aliasAPI api.RoomserverAliasAPI,
producer *producers.RoomserverProducer, producer *producers.RoomserverProducer,
keys gomatrixserverlib.KeyRing, keys gomatrixserverlib.KeyRing,
federation *gomatrixserverlib.FederationClient, federation *gomatrixserverlib.FederationClient,
@ -48,7 +48,7 @@ func Setup(
v1fedmux := apiMux.PathPrefix(pathPrefixV1Federation).Subrouter() v1fedmux := apiMux.PathPrefix(pathPrefixV1Federation).Subrouter()
localKeys := common.MakeExternalAPI("localkeys", func(req *http.Request) util.JSONResponse { localKeys := common.MakeExternalAPI("localkeys", func(req *http.Request) util.JSONResponse {
return readers.LocalKeys(cfg) return LocalKeys(cfg)
}) })
// Ignore the {keyID} argument as we only have a single server key so we always // Ignore the {keyID} argument as we only have a single server key so we always
@ -100,16 +100,34 @@ func Setup(
"federation_get_event", cfg.Matrix.ServerName, keys, "federation_get_event", cfg.Matrix.ServerName, keys,
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest) util.JSONResponse { func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest) util.JSONResponse {
vars := mux.Vars(httpReq) vars := mux.Vars(httpReq)
return readers.GetEvent( return GetEvent(
httpReq.Context(), request, cfg, query, time.Now(), keys, vars["eventID"], httpReq.Context(), request, cfg, query, time.Now(), keys, vars["eventID"],
) )
}, },
)).Methods("GET") )).Methods("GET")
v1fedmux.Handle("/query/directory/", common.MakeFedAPI(
"federation_query_room_alias", cfg.Matrix.ServerName, keys,
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest) util.JSONResponse {
return RoomAliasToID(
httpReq, federation, cfg, aliasAPI,
)
},
)).Methods("GET")
v1fedmux.Handle("/query/profile", common.MakeFedAPI(
"federation_query_profile", cfg.Matrix.ServerName, keys,
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest) util.JSONResponse {
return GetProfile(
httpReq, accountDB, cfg,
)
},
)).Methods("GET")
v1fedmux.Handle("/version", common.MakeExternalAPI( v1fedmux.Handle("/version", common.MakeExternalAPI(
"federation_version", "federation_version",
func(httpReq *http.Request) util.JSONResponse { func(httpReq *http.Request) util.JSONResponse {
return readers.Version() return Version()
}, },
)).Methods("GET") )).Methods("GET")
} }

View file

@ -170,11 +170,7 @@ func (t *txnReq) processEvent(e gomatrixserverlib.Event) error {
// TODO: Check that the event is allowed by its auth_events. // TODO: Check that the event is allowed by its auth_events.
// pass the event to the roomserver // pass the event to the roomserver
if err := t.producer.SendEvents(t.context, []gomatrixserverlib.Event{e}, api.DoNotSendToOtherServers); err != nil { return t.producer.SendEvents(t.context, []gomatrixserverlib.Event{e}, api.DoNotSendToOtherServers)
return err
}
return nil
} }
func checkAllowedByState(e gomatrixserverlib.Event, stateEvents []gomatrixserverlib.Event) error { func checkAllowedByState(e gomatrixserverlib.Event, stateEvents []gomatrixserverlib.Event) error {
@ -218,8 +214,5 @@ func (t *txnReq) processEventWithMissingState(e gomatrixserverlib.Event) error {
return err return err
} }
// pass the event along with the state to the roomserver // pass the event along with the state to the roomserver
if err := t.producer.SendEventWithState(t.context, state, e); err != nil { return t.producer.SendEventWithState(t.context, state, e)
return err
}
return nil
} }

View file

@ -287,7 +287,7 @@ func buildMembershipEvent(
// them responded with an error. // them responded with an error.
func sendToRemoteServer( func sendToRemoteServer(
ctx context.Context, inv invite, ctx context.Context, inv invite,
federation *gomatrixserverlib.FederationClient, cfg config.Dendrite, federation *gomatrixserverlib.FederationClient, _ config.Dendrite,
builder gomatrixserverlib.EventBuilder, builder gomatrixserverlib.EventBuilder,
) (err error) { ) (err error) {
remoteServers := make([]gomatrixserverlib.ServerName, 2) remoteServers := make([]gomatrixserverlib.ServerName, 2)

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package readers package routing
import ( import (
"github.com/matrix-org/util" "github.com/matrix-org/util"

View file

@ -19,7 +19,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/federationsender/queue" "github.com/matrix-org/dendrite/federationsender/queue"
@ -27,6 +26,7 @@ import (
"github.com/matrix-org/dendrite/federationsender/types" "github.com/matrix-org/dendrite/federationsender/types"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
log "github.com/sirupsen/logrus"
sarama "gopkg.in/Shopify/sarama.v1" sarama "gopkg.in/Shopify/sarama.v1"
) )
@ -134,6 +134,14 @@ func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent) err
return err return err
} }
if oldJoinedHosts == nil {
// This means that there is nothing to update as this is a duplicate
// message.
// This can happen if dendrite crashed between reading the message and
// persisting the stream position.
return nil
}
if ore.SendAsServer == api.DoNotSendToOtherServers { if ore.SendAsServer == api.DoNotSendToOtherServers {
// Ignore event that we don't need to send anywhere. // Ignore event that we don't need to send anywhere.
return nil return nil
@ -146,13 +154,9 @@ func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent) err
} }
// Send the event. // Send the event.
if err = s.queues.SendEvent( return s.queues.SendEvent(
&ore.Event, gomatrixserverlib.ServerName(ore.SendAsServer), joinedHostsAtEvent, &ore.Event, gomatrixserverlib.ServerName(ore.SendAsServer), joinedHostsAtEvent,
); err != nil { )
return err
}
return nil
} }
// joinedHostsAtEvent works out a list of matrix servers that were joined to // joinedHostsAtEvent works out a list of matrix servers that were joined to

View file

@ -20,8 +20,8 @@ import (
"sync" "sync"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
log "github.com/sirupsen/logrus"
) )
// destinationQueue is a queue of events for a single destination. // destinationQueue is a queue of events for a single destination.

View file

@ -18,8 +18,8 @@ import (
"fmt" "fmt"
"sync" "sync"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
log "github.com/sirupsen/logrus"
) )
// OutgoingQueues is a collection of queues for sending transactions to other // OutgoingQueues is a collection of queues for sending transactions to other

View file

@ -54,15 +54,14 @@ func (d *Database) prepare() error {
return err return err
} }
if err = d.PartitionOffsetStatements.Prepare(d.db, "federationsender"); err != nil { return d.PartitionOffsetStatements.Prepare(d.db, "federationsender")
return err
}
return nil
} }
// UpdateRoom updates the joined hosts for a room and returns what the joined // UpdateRoom updates the joined hosts for a room and returns what the joined
// hosts were before the update. // hosts were before the update, or nil if this was a duplicate message.
// This is called when we receive a message from kafka, so we pass in
// oldEventID and newEventID to check that we haven't missed any messages or
// this isn't a duplicate message.
func (d *Database) UpdateRoom( func (d *Database) UpdateRoom(
ctx context.Context, ctx context.Context,
roomID, oldEventID, newEventID string, roomID, oldEventID, newEventID string,
@ -70,22 +69,34 @@ func (d *Database) UpdateRoom(
removeHosts []string, removeHosts []string,
) (joinedHosts []types.JoinedHost, err error) { ) (joinedHosts []types.JoinedHost, err error) {
err = common.WithTransaction(d.db, func(txn *sql.Tx) error { err = common.WithTransaction(d.db, func(txn *sql.Tx) error {
if err = d.insertRoom(ctx, txn, roomID); err != nil { err = d.insertRoom(ctx, txn, roomID)
if err != nil {
return err return err
} }
lastSentEventID, err := d.selectRoomForUpdate(ctx, txn, roomID) lastSentEventID, err := d.selectRoomForUpdate(ctx, txn, roomID)
if err != nil { if err != nil {
return err return err
} }
if lastSentEventID == newEventID {
// We've handled this message before, so let's just ignore it.
// We can only get a duplicate for the last message we processed,
// so its enough just to compare the newEventID with lastSentEventID
return nil
}
if lastSentEventID != oldEventID { if lastSentEventID != oldEventID {
return types.EventIDMismatchError{ return types.EventIDMismatchError{
DatabaseID: lastSentEventID, RoomServerID: oldEventID, DatabaseID: lastSentEventID, RoomServerID: oldEventID,
} }
} }
joinedHosts, err = d.selectJoinedHosts(ctx, txn, roomID) joinedHosts, err = d.selectJoinedHosts(ctx, txn, roomID)
if err != nil { if err != nil {
return err return err
} }
for _, add := range addHosts { for _, add := range addHosts {
err = d.insertJoinedHosts(ctx, txn, roomID, add.MemberEventID, add.ServerName) err = d.insertJoinedHosts(ctx, txn, roomID, add.MemberEventID, add.ServerName)
if err != nil { if err != nil {

View file

@ -25,9 +25,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/dendrite/mediaapi/types"
log "github.com/sirupsen/logrus"
) )
// GetPathFromBase64Hash evaluates the path to a media file from its Base64Hash // GetPathFromBase64Hash evaluates the path to a media file from its Base64Hash

View file

@ -28,7 +28,6 @@ import (
"strings" "strings"
"sync" "sync"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/fileutils"
@ -38,6 +37,7 @@ import (
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus"
) )
const mediaIDCharacters = "A-Za-z0-9_=-" const mediaIDCharacters = "A-Za-z0-9_=-"

View file

@ -22,7 +22,6 @@ import (
"net/url" "net/url"
"path" "path"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/fileutils"
@ -31,6 +30,7 @@ import (
"github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/dendrite/mediaapi/types"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
log "github.com/sirupsen/logrus"
) )
// uploadRequest metadata included in or derivable from an upload request // uploadRequest metadata included in or derivable from an upload request
@ -161,14 +161,10 @@ func (r *uploadRequest) doUpload(
} }
} }
if resErr := r.storeFileAndMetadata( return r.storeFileAndMetadata(
ctx, tmpDir, cfg.Media.AbsBasePath, db, cfg.Media.ThumbnailSizes, ctx, tmpDir, cfg.Media.AbsBasePath, db, cfg.Media.ThumbnailSizes,
activeThumbnailGeneration, cfg.Media.MaxThumbnailGenerators, activeThumbnailGeneration, cfg.Media.MaxThumbnailGenerators,
); resErr != nil { )
return resErr
}
return nil
} }
// Validate validates the uploadRequest fields // Validate validates the uploadRequest fields

View file

@ -23,15 +23,13 @@ type statements struct {
thumbnail thumbnailStatements thumbnail thumbnailStatements
} }
func (s *statements) prepare(db *sql.DB) error { func (s *statements) prepare(db *sql.DB) (err error) {
var err error
if err = s.media.prepare(db); err != nil { if err = s.media.prepare(db); err != nil {
return err return
} }
if err = s.thumbnail.prepare(db); err != nil { if err = s.thumbnail.prepare(db); err != nil {
return err return
} }
return nil return
} }

View file

@ -22,10 +22,10 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/mediaapi/storage" "github.com/matrix-org/dendrite/mediaapi/storage"
"github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/dendrite/mediaapi/types"
log "github.com/sirupsen/logrus"
) )
type thumbnailFitness struct { type thumbnailFitness struct {
@ -89,7 +89,7 @@ func SelectThumbnail(desired types.ThumbnailSize, thumbnails []*types.ThumbnailM
} }
// getActiveThumbnailGeneration checks for active thumbnail generation // getActiveThumbnailGeneration checks for active thumbnail generation
func getActiveThumbnailGeneration(dst types.Path, config types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, logger *log.Entry) (isActive bool, busy bool, errorReturn error) { func getActiveThumbnailGeneration(dst types.Path, _ types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, logger *log.Entry) (isActive bool, busy bool, errorReturn error) {
// Check if there is active thumbnail generation. // Check if there is active thumbnail generation.
activeThumbnailGeneration.Lock() activeThumbnailGeneration.Lock()
defer activeThumbnailGeneration.Unlock() defer activeThumbnailGeneration.Unlock()
@ -119,7 +119,7 @@ func getActiveThumbnailGeneration(dst types.Path, config types.ThumbnailSize, ac
// broadcastGeneration broadcasts that thumbnail generation completed and the error to all waiting goroutines // broadcastGeneration broadcasts that thumbnail generation completed and the error to all waiting goroutines
// Note: This should only be called by the owner of the activeThumbnailGenerationResult // Note: This should only be called by the owner of the activeThumbnailGenerationResult
func broadcastGeneration(dst types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, config types.ThumbnailSize, errorReturn error, logger *log.Entry) { func broadcastGeneration(dst types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, _ types.ThumbnailSize, errorReturn error, logger *log.Entry) {
activeThumbnailGeneration.Lock() activeThumbnailGeneration.Lock()
defer activeThumbnailGeneration.Unlock() defer activeThumbnailGeneration.Unlock()
if activeThumbnailGenerationResult, ok := activeThumbnailGeneration.PathToResult[string(dst)]; ok { if activeThumbnailGenerationResult, ok := activeThumbnailGeneration.PathToResult[string(dst)]; ok {

View file

@ -21,10 +21,10 @@ import (
"os" "os"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/mediaapi/storage" "github.com/matrix-org/dendrite/mediaapi/storage"
"github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/dendrite/mediaapi/types"
log "github.com/sirupsen/logrus"
"gopkg.in/h2non/bimg.v1" "gopkg.in/h2non/bimg.v1"
) )

View file

@ -28,11 +28,11 @@ import (
"os" "os"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/mediaapi/storage" "github.com/matrix-org/dendrite/mediaapi/storage"
"github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/dendrite/mediaapi/types"
"github.com/nfnt/resize" "github.com/nfnt/resize"
log "github.com/sirupsen/logrus"
) )
// GenerateThumbnails generates the configured thumbnail sizes for the source file // GenerateThumbnails generates the configured thumbnail sizes for the source file

View file

@ -18,11 +18,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/publicroomsapi/storage" "github.com/matrix-org/dendrite/publicroomsapi/storage"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
log "github.com/sirupsen/logrus"
sarama "gopkg.in/Shopify/sarama.v1" sarama "gopkg.in/Shopify/sarama.v1"
) )

View file

@ -79,11 +79,7 @@ func (r *RoomserverAliasAPI) SetRoomAlias(
// At this point we've already committed the alias to the database so we // At this point we've already committed the alias to the database so we
// shouldn't cancel this request. // shouldn't cancel this request.
// TODO: Ensure that we send unsent events when if server restarts. // TODO: Ensure that we send unsent events when if server restarts.
if err := r.sendUpdatedAliasesEvent(context.TODO(), request.UserID, request.RoomID); err != nil { return r.sendUpdatedAliasesEvent(context.TODO(), request.UserID, request.RoomID)
return err
}
return nil
} }
// GetAliasRoomID implements api.RoomserverAliasAPI // GetAliasRoomID implements api.RoomserverAliasAPI
@ -123,11 +119,7 @@ func (r *RoomserverAliasAPI) RemoveRoomAlias(
// At this point we've already committed the alias to the database so we // At this point we've already committed the alias to the database so we
// shouldn't cancel this request. // shouldn't cancel this request.
// TODO: Ensure that we send unsent events when if server restarts. // TODO: Ensure that we send unsent events when if server restarts.
if err := r.sendUpdatedAliasesEvent(context.TODO(), request.UserID, roomID); err != nil { return r.sendUpdatedAliasesEvent(context.TODO(), request.UserID, roomID)
return err
}
return nil
} }
type roomAliasesContent struct { type roomAliasesContent struct {

View file

@ -129,11 +129,7 @@ func processRoomEvent(
} }
// Update the extremities of the event graph for the room // Update the extremities of the event graph for the room
if err := updateLatestEvents(ctx, db, ow, roomNID, stateAtEvent, event, input.SendAsServer); err != nil { return updateLatestEvents(ctx, db, ow, roomNID, stateAtEvent, event, input.SendAsServer)
return err
}
return nil
} }
func processInviteEvent( func processInviteEvent(

View file

@ -162,11 +162,7 @@ func (u *latestEventsUpdater) doUpdateLatestEvents() error {
return err return err
} }
if err = u.updater.MarkEventAsSent(u.stateAtEvent.EventNID); err != nil { return u.updater.MarkEventAsSent(u.stateAtEvent.EventNID)
return err
}
return nil
} }
func (u *latestEventsUpdater) latestState() error { func (u *latestEventsUpdater) latestState() error {

View file

@ -18,11 +18,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/syncapi/storage" "github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/sync" "github.com/matrix-org/dendrite/syncapi/sync"
log "github.com/sirupsen/logrus"
sarama "gopkg.in/Shopify/sarama.v1" sarama "gopkg.in/Shopify/sarama.v1"
) )

View file

@ -19,7 +19,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
@ -27,6 +26,7 @@ import (
"github.com/matrix-org/dendrite/syncapi/sync" "github.com/matrix-org/dendrite/syncapi/sync"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
log "github.com/sirupsen/logrus"
sarama "gopkg.in/Shopify/sarama.v1" sarama "gopkg.in/Shopify/sarama.v1"
) )

View file

@ -21,6 +21,7 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/devices" "github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/sync" "github.com/matrix-org/dendrite/syncapi/sync"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
@ -28,7 +29,7 @@ import (
const pathPrefixR0 = "/_matrix/client/r0" const pathPrefixR0 = "/_matrix/client/r0"
// Setup configures the given mux with sync-server listeners // Setup configures the given mux with sync-server listeners
func Setup(apiMux *mux.Router, srp *sync.RequestPool, deviceDB *devices.Database) { func Setup(apiMux *mux.Router, srp *sync.RequestPool, syncDB *storage.SyncServerDatabase, deviceDB *devices.Database) {
r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
r0mux.Handle("/sync", common.MakeAuthAPI("sync", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { r0mux.Handle("/sync", common.MakeAuthAPI("sync", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
@ -37,16 +38,16 @@ func Setup(apiMux *mux.Router, srp *sync.RequestPool, deviceDB *devices.Database
r0mux.Handle("/rooms/{roomID}/state", common.MakeAuthAPI("room_state", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { r0mux.Handle("/rooms/{roomID}/state", common.MakeAuthAPI("room_state", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req) vars := mux.Vars(req)
return srp.OnIncomingStateRequest(req, vars["roomID"]) return OnIncomingStateRequest(req, syncDB, vars["roomID"])
})).Methods("GET") })).Methods("GET")
r0mux.Handle("/rooms/{roomID}/state/{type}", common.MakeAuthAPI("room_state", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { r0mux.Handle("/rooms/{roomID}/state/{type}", common.MakeAuthAPI("room_state", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req) vars := mux.Vars(req)
return srp.OnIncomingStateTypeRequest(req, vars["roomID"], vars["type"], "") return OnIncomingStateTypeRequest(req, syncDB, vars["roomID"], vars["type"], "")
})).Methods("GET") })).Methods("GET")
r0mux.Handle("/rooms/{roomID}/state/{type}/{stateKey}", common.MakeAuthAPI("room_state", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { r0mux.Handle("/rooms/{roomID}/state/{type}/{stateKey}", common.MakeAuthAPI("room_state", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req) vars := mux.Vars(req)
return srp.OnIncomingStateTypeRequest(req, vars["roomID"], vars["type"], vars["stateKey"]) return OnIncomingStateTypeRequest(req, syncDB, vars["roomID"], vars["type"], vars["stateKey"])
})).Methods("GET") })).Methods("GET")
} }

View file

@ -0,0 +1,118 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"encoding/json"
"net/http"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
log "github.com/sirupsen/logrus"
)
type stateEventInStateResp struct {
gomatrixserverlib.ClientEvent
PrevContent json.RawMessage `json:"prev_content,omitempty"`
ReplacesState string `json:"replaces_state,omitempty"`
}
// OnIncomingStateRequest is called when a client makes a /rooms/{roomID}/state
// request. It will fetch all the state events from the specified room and will
// append the necessary keys to them if applicable before returning them.
// Returns an error if something went wrong in the process.
// TODO: Check if the user is in the room. If not, check if the room's history
// is publicly visible. Current behaviour is returning an empty array if the
// user cannot see the room's history.
func OnIncomingStateRequest(req *http.Request, db *storage.SyncServerDatabase, roomID string) util.JSONResponse {
// TODO(#287): Auth request and handle the case where the user has left (where
// we should return the state at the poin they left)
stateEvents, err := db.GetStateEventsForRoom(req.Context(), roomID)
if err != nil {
return httputil.LogThenError(req, err)
}
resp := []stateEventInStateResp{}
// Fill the prev_content and replaces_state keys if necessary
for _, event := range stateEvents {
stateEvent := stateEventInStateResp{
ClientEvent: gomatrixserverlib.ToClientEvent(event, gomatrixserverlib.FormatAll),
}
var prevEventRef types.PrevEventRef
if len(event.Unsigned()) > 0 {
if err := json.Unmarshal(event.Unsigned(), &prevEventRef); err != nil {
return httputil.LogThenError(req, err)
}
// Fills the previous state event ID if the state event replaces another
// state event
if len(prevEventRef.ReplacesState) > 0 {
stateEvent.ReplacesState = prevEventRef.ReplacesState
}
// Fill the previous event if the state event references a previous event
if prevEventRef.PrevContent != nil {
stateEvent.PrevContent = prevEventRef.PrevContent
}
}
resp = append(resp, stateEvent)
}
return util.JSONResponse{
Code: 200,
JSON: resp,
}
}
// OnIncomingStateTypeRequest is called when a client makes a
// /rooms/{roomID}/state/{type}/{statekey} request. It will look in current
// state to see if there is an event with that type and state key, if there
// is then (by default) we return the content, otherwise a 404.
func OnIncomingStateTypeRequest(req *http.Request, db *storage.SyncServerDatabase, roomID string, evType, stateKey string) util.JSONResponse {
// TODO(#287): Auth request and handle the case where the user has left (where
// we should return the state at the poin they left)
logger := util.GetLogger(req.Context())
logger.WithFields(log.Fields{
"roomID": roomID,
"evType": evType,
"stateKey": stateKey,
}).Info("Fetching state")
event, err := db.GetStateEvent(req.Context(), roomID, evType, stateKey)
if err != nil {
return httputil.LogThenError(req, err)
}
if event == nil {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound("cannot find state"),
}
}
stateEvent := stateEventInStateResp{
ClientEvent: gomatrixserverlib.ToClientEvent(*event, gomatrixserverlib.FormatAll),
}
return util.JSONResponse{
Code: 200,
JSON: stateEvent.Content,
}
}

View file

@ -18,11 +18,11 @@ import (
"context" "context"
"database/sql" "database/sql"
log "github.com/sirupsen/logrus"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
log "github.com/sirupsen/logrus"
) )
const outputRoomEventsSchema = ` const outputRoomEventsSchema = `

View file

@ -229,44 +229,10 @@ func (d *SyncServerDatabase) IncrementalSync(
res := types.NewResponse(toPos) res := types.NewResponse(toPos)
for _, delta := range deltas { for _, delta := range deltas {
endPos := toPos err = d.addRoomDeltaToResponse(ctx, txn, fromPos, toPos, delta, numRecentEventsPerRoom, res)
if delta.membershipPos > 0 && delta.membership == "leave" {
// make sure we don't leak recent events after the leave event.
// TODO: History visibility makes this somewhat complex to handle correctly. For example:
// TODO: This doesn't work for join -> leave in a single /sync request (see events prior to join).
// TODO: This will fail on join -> leave -> sensitive msg -> join -> leave
// in a single /sync request
// This is all "okay" assuming history_visibility == "shared" which it is by default.
endPos = delta.membershipPos
}
var recentStreamEvents []streamEvent
recentStreamEvents, err = d.events.selectRecentEvents(
ctx, txn, delta.roomID, fromPos, endPos, numRecentEventsPerRoom,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
recentEvents := streamEventsToEvents(recentStreamEvents)
delta.stateEvents = removeDuplicates(delta.stateEvents, recentEvents) // roll back
switch delta.membership {
case "join":
jr := types.NewJoinResponse()
jr.Timeline.Events = gomatrixserverlib.ToClientEvents(recentEvents, gomatrixserverlib.FormatSync)
jr.Timeline.Limited = false // TODO: if len(events) >= numRecents + 1 and then set limited:true
jr.State.Events = gomatrixserverlib.ToClientEvents(delta.stateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Join[delta.roomID] = *jr
case "leave":
fallthrough // transitions to leave are the same as ban
case "ban":
// TODO: recentEvents may contain events that this user is not allowed to see because they are
// no longer in the room.
lr := types.NewLeaveResponse()
lr.Timeline.Events = gomatrixserverlib.ToClientEvents(recentEvents, gomatrixserverlib.FormatSync)
lr.Timeline.Limited = false // TODO: if len(events) >= numRecents + 1 and then set limited:true
lr.State.Events = gomatrixserverlib.ToClientEvents(delta.stateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Leave[delta.roomID] = *lr
}
} }
// TODO: This should be done in getStateDeltas // TODO: This should be done in getStateDeltas
@ -418,6 +384,60 @@ func (d *SyncServerDatabase) addInvitesToResponse(
return nil return nil
} }
// addRoomDeltaToResponse adds a room state delta to a sync response
func (d *SyncServerDatabase) addRoomDeltaToResponse(
ctx context.Context, txn *sql.Tx,
fromPos, toPos types.StreamPosition,
delta stateDelta,
numRecentEventsPerRoom int,
res *types.Response,
) error {
endPos := toPos
if delta.membershipPos > 0 && delta.membership == "leave" {
// make sure we don't leak recent events after the leave event.
// TODO: History visibility makes this somewhat complex to handle correctly. For example:
// TODO: This doesn't work for join -> leave in a single /sync request (see events prior to join).
// TODO: This will fail on join -> leave -> sensitive msg -> join -> leave
// in a single /sync request
// This is all "okay" assuming history_visibility == "shared" which it is by default.
endPos = delta.membershipPos
}
recentStreamEvents, err := d.events.selectRecentEvents(
ctx, txn, delta.roomID, fromPos, endPos, numRecentEventsPerRoom,
)
if err != nil {
return err
}
recentEvents := streamEventsToEvents(recentStreamEvents)
delta.stateEvents = removeDuplicates(delta.stateEvents, recentEvents) // roll back
// Don't bother appending empty room entries
if len(recentEvents) == 0 && len(delta.stateEvents) == 0 {
return nil
}
switch delta.membership {
case "join":
jr := types.NewJoinResponse()
jr.Timeline.Events = gomatrixserverlib.ToClientEvents(recentEvents, gomatrixserverlib.FormatSync)
jr.Timeline.Limited = false // TODO: if len(events) >= numRecents + 1 and then set limited:true
jr.State.Events = gomatrixserverlib.ToClientEvents(delta.stateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Join[delta.roomID] = *jr
case "leave":
fallthrough // transitions to leave are the same as ban
case "ban":
// TODO: recentEvents may contain events that this user is not allowed to see because they are
// no longer in the room.
lr := types.NewLeaveResponse()
lr.Timeline.Events = gomatrixserverlib.ToClientEvents(recentEvents, gomatrixserverlib.FormatSync)
lr.Timeline.Limited = false // TODO: if len(events) >= numRecents + 1 and then set limited:true
lr.State.Events = gomatrixserverlib.ToClientEvents(delta.stateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Leave[delta.roomID] = *lr
}
return nil
}
// fetchStateEvents converts the set of event IDs into a set of events. It will fetch any which are missing from the database. // fetchStateEvents converts the set of event IDs into a set of events. It will fetch any which are missing from the database.
// Returns a map of room ID to list of events. // Returns a map of room ID to list of events.
func (d *SyncServerDatabase) fetchStateEvents( func (d *SyncServerDatabase) fetchStateEvents(

View file

@ -17,11 +17,12 @@ package sync
import ( import (
"context" "context"
"sync" "sync"
"time"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/syncapi/storage" "github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
log "github.com/sirupsen/logrus"
) )
// Notifier will wake up sleeping requests when there is some new data. // Notifier will wake up sleeping requests when there is some new data.
@ -38,6 +39,8 @@ type Notifier struct {
currPos types.StreamPosition currPos types.StreamPosition
// A map of user_id => UserStream which can be used to wake a given user's /sync request. // A map of user_id => UserStream which can be used to wake a given user's /sync request.
userStreams map[string]*UserStream userStreams map[string]*UserStream
// The last time we cleaned out stale entries from the userStreams map
lastCleanUpTime time.Time
} }
// NewNotifier creates a new notifier set to the given stream position. // NewNotifier creates a new notifier set to the given stream position.
@ -49,6 +52,7 @@ func NewNotifier(pos types.StreamPosition) *Notifier {
roomIDToJoinedUsers: make(map[string]userIDSet), roomIDToJoinedUsers: make(map[string]userIDSet),
userStreams: make(map[string]*UserStream), userStreams: make(map[string]*UserStream),
streamLock: &sync.Mutex{}, streamLock: &sync.Mutex{},
lastCleanUpTime: time.Now(),
} }
} }
@ -63,6 +67,8 @@ func (n *Notifier) OnNewEvent(ev *gomatrixserverlib.Event, userID string, pos ty
defer n.streamLock.Unlock() defer n.streamLock.Unlock()
n.currPos = pos n.currPos = pos
n.removeEmptyUserStreams()
if ev != nil { if ev != nil {
// Map this event's room_id to a list of joined users, and wake them up. // Map this event's room_id to a list of joined users, and wake them up.
userIDs := n.joinedUsers(ev.RoomID()) userIDs := n.joinedUsers(ev.RoomID())
@ -100,8 +106,10 @@ func (n *Notifier) OnNewEvent(ev *gomatrixserverlib.Event, userID string, pos ty
} }
} }
// WaitForEvents blocks until there are new events for this request. // GetListener returns a UserStreamListener that can be used to wait for
func (n *Notifier) WaitForEvents(req syncRequest) types.StreamPosition { // updates for a user. Must be closed.
// notify for anything before sincePos
func (n *Notifier) GetListener(req syncRequest) UserStreamListener {
// Do what synapse does: https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/notifier.py#L298 // Do what synapse does: https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/notifier.py#L298
// - Bucket request into a lookup map keyed off a list of joined room IDs and separately a user ID // - Bucket request into a lookup map keyed off a list of joined room IDs and separately a user ID
// - Incoming events wake requests for a matching room ID // - Incoming events wake requests for a matching room ID
@ -110,25 +118,12 @@ func (n *Notifier) WaitForEvents(req syncRequest) types.StreamPosition {
// TODO: v1 /events 'peeking' has an 'explicit room ID' which is also tracked, // TODO: v1 /events 'peeking' has an 'explicit room ID' which is also tracked,
// but given we don't do /events, let's pretend it doesn't exist. // but given we don't do /events, let's pretend it doesn't exist.
// In a guard, check if the /sync request should block, and block it until we get woken up
n.streamLock.Lock() n.streamLock.Lock()
currentPos := n.currPos defer n.streamLock.Unlock()
// TODO: We increment the stream position for any event, so it's possible that we return immediately n.removeEmptyUserStreams()
// with a pos which contains no new events for this user. We should probably re-wait for events
// automatically in this case.
if req.since != currentPos {
n.streamLock.Unlock()
return currentPos
}
// wait to be woken up, and then re-check the stream position return n.fetchUserStream(req.userID, true).GetListener(req.ctx)
req.log.WithField("user_id", req.userID).Info("Waiting for event")
// give up the stream lock prior to waiting on the user lock
stream := n.fetchUserStream(req.userID, true)
n.streamLock.Unlock()
return stream.Wait(currentPos)
} }
// Load the membership states required to notify users correctly. // Load the membership states required to notify users correctly.
@ -141,6 +136,11 @@ func (n *Notifier) Load(ctx context.Context, db *storage.SyncServerDatabase) err
return nil return nil
} }
// CurrentPosition returns the current stream position
func (n *Notifier) CurrentPosition() types.StreamPosition {
return n.currPos
}
// setUsersJoinedToRooms marks the given users as 'joined' to the given rooms, such that new events from // setUsersJoinedToRooms marks the given users as 'joined' to the given rooms, such that new events from
// these rooms will wake the given users /sync requests. This should be called prior to ANY calls to // these rooms will wake the given users /sync requests. This should be called prior to ANY calls to
// OnNewEvent (eg on startup) to prevent racing. // OnNewEvent (eg on startup) to prevent racing.
@ -171,7 +171,7 @@ func (n *Notifier) fetchUserStream(userID string, makeIfNotExists bool) *UserStr
stream, ok := n.userStreams[userID] stream, ok := n.userStreams[userID]
if !ok && makeIfNotExists { if !ok && makeIfNotExists {
// TODO: Unbounded growth of streams (1 per user) // TODO: Unbounded growth of streams (1 per user)
stream = NewUserStream(userID) stream = NewUserStream(userID, n.currPos)
n.userStreams[userID] = stream n.userStreams[userID] = stream
} }
return stream return stream
@ -201,6 +201,29 @@ func (n *Notifier) joinedUsers(roomID string) (userIDs []string) {
return n.roomIDToJoinedUsers[roomID].values() return n.roomIDToJoinedUsers[roomID].values()
} }
// removeEmptyUserStreams iterates through the user stream map and removes any
// that have been empty for a certain amount of time. This is a crude way of
// ensuring that the userStreams map doesn't grow forver.
// This should be called when the notifier gets called for whatever reason,
// the function itself is responsible for ensuring it doesn't iterate too
// often.
// NB: Callers should have locked the mutex before calling this function.
func (n *Notifier) removeEmptyUserStreams() {
// Only clean up now and again
now := time.Now()
if n.lastCleanUpTime.Add(time.Minute).After(now) {
return
}
n.lastCleanUpTime = now
deleteBefore := now.Add(-5 * time.Minute)
for key, value := range n.userStreams {
if value.TimeOfLastNonEmpty().Before(deleteBefore) {
delete(n.userStreams, key)
}
}
}
// A string set, mainly existing for improving clarity of structs in this file. // A string set, mainly existing for improving clarity of structs in this file.
type userIDSet map[string]bool type userIDSet map[string]bool

View file

@ -256,24 +256,22 @@ func TestNewEventAndWasPreviouslyJoinedToRoom(t *testing.T) {
// same as Notifier.WaitForEvents but with a timeout. // same as Notifier.WaitForEvents but with a timeout.
func waitForEvents(n *Notifier, req syncRequest) (types.StreamPosition, error) { func waitForEvents(n *Notifier, req syncRequest) (types.StreamPosition, error) {
done := make(chan types.StreamPosition, 1) listener := n.GetListener(req)
go func() { defer listener.Close()
newPos := n.WaitForEvents(req)
done <- newPos
close(done)
}()
select { select {
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
return types.StreamPosition(0), fmt.Errorf( return types.StreamPosition(0), fmt.Errorf(
"waitForEvents timed out waiting for %s (pos=%d)", req.userID, req.since, "waitForEvents timed out waiting for %s (pos=%d)", req.userID, req.since,
) )
case p := <-done: case <-listener.GetNotifyChannel(req.since):
p := listener.GetStreamPosition()
return p, nil return p, nil
} }
} }
// Wait until something is Wait()ing on the user stream. // Wait until something is Wait()ing on the user stream.
func waitForBlocking(s *UserStream, numBlocking int) { func waitForBlocking(s *UserStream, numBlocking uint) {
for numBlocking != s.NumWaiting() { for numBlocking != s.NumWaiting() {
// This is horrible but I don't want to add a signalling mechanism JUST for testing. // This is horrible but I don't want to add a signalling mechanism JUST for testing.
time.Sleep(1 * time.Microsecond) time.Sleep(1 * time.Microsecond)
@ -288,5 +286,6 @@ func newTestSyncRequest(userID string, since types.StreamPosition) syncRequest {
wantFullState: false, wantFullState: false,
limit: defaultTimelineLimit, limit: defaultTimelineLimit,
log: util.GetLogger(context.TODO()), log: util.GetLogger(context.TODO()),
ctx: context.TODO(),
} }
} }

View file

@ -20,9 +20,9 @@ import (
"strconv" "strconv"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/util" "github.com/matrix-org/util"
log "github.com/sirupsen/logrus"
) )
const defaultSyncTimeout = time.Duration(30) * time.Second const defaultSyncTimeout = time.Duration(30) * time.Second

View file

@ -15,11 +15,9 @@
package sync package sync
import ( import (
"encoding/json"
"net/http" "net/http"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
@ -28,6 +26,7 @@ import (
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
log "github.com/sirupsen/logrus"
) )
// RequestPool manages HTTP long-poll connections for /sync // RequestPool manages HTTP long-poll connections for /sync
@ -62,146 +61,78 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *authtype
"timeout": syncReq.timeout, "timeout": syncReq.timeout,
}).Info("Incoming /sync request") }).Info("Incoming /sync request")
// Fork off 2 goroutines: one to do the work, and one to serve as a timeout. currPos := rp.notifier.CurrentPosition()
// Whichever returns first is the one we will serve back to the client.
timeoutChan := make(chan struct{})
timer := time.AfterFunc(syncReq.timeout, func() {
close(timeoutChan) // signal that the timeout has expired
})
done := make(chan util.JSONResponse) // If this is an initial sync or timeout=0 we return immediately
go func() { if syncReq.since == types.StreamPosition(0) || syncReq.timeout == 0 {
currentPos := rp.notifier.WaitForEvents(*syncReq) syncData, err := rp.currentSyncForUser(*syncReq, currPos)
// We stop the timer BEFORE calculating the response so the cpu work
// done to calculate the response is not timed. This stops us from
// doing lots of work then timing out and sending back an empty response.
timer.Stop()
syncData, err := rp.currentSyncForUser(*syncReq, currentPos)
var res util.JSONResponse
if err != nil { if err != nil {
res = httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} else {
syncData, err = rp.appendAccountData(syncData, device.UserID, *syncReq, currentPos)
if err != nil {
res = httputil.LogThenError(req, err)
} else {
res = util.JSONResponse{
Code: 200,
JSON: syncData,
}
}
} }
done <- res
close(done)
}()
select {
case <-timeoutChan: // timeout fired
return util.JSONResponse{ return util.JSONResponse{
Code: 200, Code: 200,
JSON: types.NewResponse(syncReq.since), JSON: syncData,
} }
case res := <-done: // received a response
return res
}
}
type stateEventInStateResp struct {
gomatrixserverlib.ClientEvent
PrevContent json.RawMessage `json:"prev_content,omitempty"`
ReplacesState string `json:"replaces_state,omitempty"`
}
// OnIncomingStateRequest is called when a client makes a /rooms/{roomID}/state
// request. It will fetch all the state events from the specified room and will
// append the necessary keys to them if applicable before returning them.
// Returns an error if something went wrong in the process.
// TODO: Check if the user is in the room. If not, check if the room's history
// is publicly visible. Current behaviour is returning an empty array if the
// user cannot see the room's history.
func (rp *RequestPool) OnIncomingStateRequest(req *http.Request, roomID string) util.JSONResponse {
// TODO(#287): Auth request and handle the case where the user has left (where
// we should return the state at the poin they left)
stateEvents, err := rp.db.GetStateEventsForRoom(req.Context(), roomID)
if err != nil {
return httputil.LogThenError(req, err)
} }
resp := []stateEventInStateResp{} // Otherwise, we wait for the notifier to tell us if something *may* have
// Fill the prev_content and replaces_state keys if necessary // happened. We loop in case it turns out that nothing did happen.
for _, event := range stateEvents {
stateEvent := stateEventInStateResp{ timer := time.NewTimer(syncReq.timeout) // case of timeout=0 is handled above
ClientEvent: gomatrixserverlib.ToClientEvent(event, gomatrixserverlib.FormatAll), defer timer.Stop()
userStreamListener := rp.notifier.GetListener(*syncReq)
defer userStreamListener.Close()
for {
select {
// Wait for notifier to wake us up
case <-userStreamListener.GetNotifyChannel(currPos):
currPos = userStreamListener.GetStreamPosition()
// Or for timeout to expire
case <-timer.C:
return util.JSONResponse{
Code: 200,
JSON: types.NewResponse(syncReq.since),
}
// Or for the request to be cancelled
case <-req.Context().Done():
return httputil.LogThenError(req, req.Context().Err())
} }
var prevEventRef types.PrevEventRef
if len(event.Unsigned()) > 0 { // Note that we don't time out during calculation of sync
if err := json.Unmarshal(event.Unsigned(), &prevEventRef); err != nil { // response. This ensures that we don't waste the hard work
return httputil.LogThenError(req, err) // of calculating the sync only to get timed out before we
} // can respond
// Fills the previous state event ID if the state event replaces another
// state event syncData, err := rp.currentSyncForUser(*syncReq, currPos)
if len(prevEventRef.ReplacesState) > 0 { if err != nil {
stateEvent.ReplacesState = prevEventRef.ReplacesState return httputil.LogThenError(req, err)
} }
// Fill the previous event if the state event references a previous event if !syncData.IsEmpty() {
if prevEventRef.PrevContent != nil { return util.JSONResponse{
stateEvent.PrevContent = prevEventRef.PrevContent Code: 200,
JSON: syncData,
} }
} }
resp = append(resp, stateEvent)
}
return util.JSONResponse{
Code: 200,
JSON: resp,
} }
} }
// OnIncomingStateTypeRequest is called when a client makes a func (rp *RequestPool) currentSyncForUser(req syncRequest, currentPos types.StreamPosition) (res *types.Response, err error) {
// /rooms/{roomID}/state/{type}/{statekey} request. It will look in current
// state to see if there is an event with that type and state key, if there
// is then (by default) we return the content, otherwise a 404.
func (rp *RequestPool) OnIncomingStateTypeRequest(req *http.Request, roomID string, evType, stateKey string) util.JSONResponse {
// TODO(#287): Auth request and handle the case where the user has left (where
// we should return the state at the poin they left)
logger := util.GetLogger(req.Context())
logger.WithFields(log.Fields{
"roomID": roomID,
"evType": evType,
"stateKey": stateKey,
}).Info("Fetching state")
event, err := rp.db.GetStateEvent(req.Context(), roomID, evType, stateKey)
if err != nil {
return httputil.LogThenError(req, err)
}
if event == nil {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound("cannot find state"),
}
}
stateEvent := stateEventInStateResp{
ClientEvent: gomatrixserverlib.ToClientEvent(*event, gomatrixserverlib.FormatAll),
}
return util.JSONResponse{
Code: 200,
JSON: stateEvent.Content,
}
}
func (rp *RequestPool) currentSyncForUser(req syncRequest, currentPos types.StreamPosition) (*types.Response, error) {
// TODO: handle ignored users // TODO: handle ignored users
if req.since == types.StreamPosition(0) { if req.since == types.StreamPosition(0) {
return rp.db.CompleteSync(req.ctx, req.userID, req.limit) res, err = rp.db.CompleteSync(req.ctx, req.userID, req.limit)
} else {
res, err = rp.db.IncrementalSync(req.ctx, req.userID, req.since, currentPos, req.limit)
} }
return rp.db.IncrementalSync(req.ctx, req.userID, req.since, currentPos, req.limit)
if err != nil {
return
}
res, err = rp.appendAccountData(res, req.userID, req, currentPos)
return
} }
func (rp *RequestPool) appendAccountData( func (rp *RequestPool) appendAccountData(

View file

@ -15,65 +15,148 @@
package sync package sync
import ( import (
"context"
"runtime"
"sync" "sync"
"time"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/util"
) )
// UserStream represents a communication mechanism between the /sync request goroutine // UserStream represents a communication mechanism between the /sync request goroutine
// and the underlying sync server goroutines. Goroutines can Wait() for a stream position and // and the underlying sync server goroutines.
// goroutines can Broadcast(streamPosition) to other goroutines. // Goroutines can get a UserStreamListener to wait for updates, and can Broadcast()
// updates.
type UserStream struct { type UserStream struct {
UserID string UserID string
// Because this is a Cond, we can notify all waiting goroutines so this works // The lock that protects changes to this struct
// across devices for the same user. Protects pos. lock sync.Mutex
cond *sync.Cond // Closed when there is an update.
// The position to broadcast to callers of Wait(). signalChannel chan struct{}
// The last stream position that there may have been an update for the suser
pos types.StreamPosition pos types.StreamPosition
// The number of goroutines blocked on Wait() - used for testing and metrics // The last time when we had some listeners waiting
numWaiting int timeOfLastChannel time.Time
// The number of listeners waiting
numWaiting uint
}
// UserStreamListener allows a sync request to wait for updates for a user.
type UserStreamListener struct {
userStream *UserStream
// Whether the stream has been closed
hasClosed bool
} }
// NewUserStream creates a new user stream // NewUserStream creates a new user stream
func NewUserStream(userID string) *UserStream { func NewUserStream(userID string, currPos types.StreamPosition) *UserStream {
return &UserStream{ return &UserStream{
UserID: userID, UserID: userID,
cond: sync.NewCond(&sync.Mutex{}), timeOfLastChannel: time.Now(),
pos: currPos,
signalChannel: make(chan struct{}),
} }
} }
// Wait blocks until there is a new stream position for this user, which is then returned. // GetListener returns UserStreamListener that a sync request can use to wait
// waitAtPos should be the position the stream thinks it should be waiting at. // for new updates with.
func (s *UserStream) Wait(waitAtPos types.StreamPosition) (pos types.StreamPosition) { // UserStreamListener must be closed
s.cond.L.Lock() func (s *UserStream) GetListener(ctx context.Context) UserStreamListener {
// Before we start blocking, we need to make sure that we didn't race with a call s.lock.Lock()
// to Broadcast() between calling Wait() and actually sleeping. We check the last defer s.lock.Unlock()
// broadcast pos to see if it is newer than the pos we are meant to wait at. If it
// is newer, something has Broadcast to this stream more recently so return immediately. s.numWaiting++ // We decrement when UserStreamListener is closed
if s.pos > waitAtPos {
pos = s.pos listener := UserStreamListener{
s.cond.L.Unlock() userStream: s,
return
} }
s.numWaiting++
s.cond.Wait() // Lets be a bit paranoid here and check that Close() is being called
pos = s.pos runtime.SetFinalizer(&listener, func(l *UserStreamListener) {
s.numWaiting-- if !l.hasClosed {
s.cond.L.Unlock() util.GetLogger(ctx).Warn("Didn't call Close on UserStreamListener")
return l.Close()
}
})
return listener
} }
// Broadcast a new stream position for this user. // Broadcast a new stream position for this user.
func (s *UserStream) Broadcast(pos types.StreamPosition) { func (s *UserStream) Broadcast(pos types.StreamPosition) {
s.cond.L.Lock() s.lock.Lock()
defer s.lock.Unlock()
s.pos = pos s.pos = pos
s.cond.L.Unlock()
s.cond.Broadcast() close(s.signalChannel)
s.signalChannel = make(chan struct{})
} }
// NumWaiting returns the number of goroutines waiting for Wait() to return. Used for metrics and testing. // NumWaiting returns the number of goroutines waiting for waiting for updates.
func (s *UserStream) NumWaiting() int { // Used for metrics and testing.
s.cond.L.Lock() func (s *UserStream) NumWaiting() uint {
defer s.cond.L.Unlock() s.lock.Lock()
defer s.lock.Unlock()
return s.numWaiting return s.numWaiting
} }
// TimeOfLastNonEmpty returns the last time that the number of waiting listeners
// was non-empty, may be time.Now() if number of waiting listeners is currently
// non-empty.
func (s *UserStream) TimeOfLastNonEmpty() time.Time {
s.lock.Lock()
defer s.lock.Unlock()
if s.numWaiting > 0 {
return time.Now()
}
return s.timeOfLastChannel
}
// GetStreamPosition returns last stream position which the UserStream was
// notified about
func (s *UserStreamListener) GetStreamPosition() types.StreamPosition {
s.userStream.lock.Lock()
defer s.userStream.lock.Unlock()
return s.userStream.pos
}
// GetNotifyChannel returns a channel that is closed when there may be an
// update for the user.
// sincePos specifies from which point we want to be notified about. If there
// has already been an update after sincePos we'll return a closed channel
// immediately.
func (s *UserStreamListener) GetNotifyChannel(sincePos types.StreamPosition) <-chan struct{} {
s.userStream.lock.Lock()
defer s.userStream.lock.Unlock()
if sincePos < s.userStream.pos {
// If the listener is behind, i.e. missed a potential update, then we
// want them to wake up immediately. We do this by returning a new
// closed stream, which returns immediately when selected.
closedChannel := make(chan struct{})
close(closedChannel)
return closedChannel
}
return s.userStream.signalChannel
}
// Close cleans up resources used
func (s *UserStreamListener) Close() {
s.userStream.lock.Lock()
defer s.userStream.lock.Unlock()
if !s.hasClosed {
s.userStream.numWaiting--
s.userStream.timeOfLastChannel = time.Now()
}
s.hasClosed = true
}

View file

@ -74,6 +74,16 @@ func NewResponse(pos StreamPosition) *Response {
return &res return &res
} }
// IsEmpty returns true if the response is empty, i.e. used to decided whether
// to return the response immediately to the client or to wait for more data.
func (r *Response) IsEmpty() bool {
return len(r.Rooms.Join) == 0 &&
len(r.Rooms.Invite) == 0 &&
len(r.Rooms.Leave) == 0 &&
len(r.AccountData.Events) == 0 &&
len(r.Presence.Events) == 0
}
// JoinResponse represents a /sync response for a room which is under the 'join' key. // JoinResponse represents a /sync response for a room which is under the 'join' key.
type JoinResponse struct { type JoinResponse struct {
State struct { State struct {

6
vendor/manifest vendored
View file

@ -10,7 +10,7 @@
{ {
"importpath": "github.com/alecthomas/gometalinter", "importpath": "github.com/alecthomas/gometalinter",
"repository": "https://github.com/alecthomas/gometalinter", "repository": "https://github.com/alecthomas/gometalinter",
"revision": "5507b26af3204e949ffe50ec08ee73e5847938e1", "revision": "0262fb20957a4c2d3bb7c834a6a125ae3884a2c6",
"branch": "master" "branch": "master"
}, },
{ {
@ -135,13 +135,13 @@
{ {
"importpath": "github.com/matrix-org/gomatrixserverlib", "importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib",
"revision": "fb17c27f65a0699b0d15f5311a530225b4aea5e0", "revision": "076933f95312aae3a9476e78d6b4118e1b45d542",
"branch": "master" "branch": "master"
}, },
{ {
"importpath": "github.com/matrix-org/naffka", "importpath": "github.com/matrix-org/naffka",
"repository": "https://github.com/matrix-org/naffka", "repository": "https://github.com/matrix-org/naffka",
"revision": "d28656e34f96a8eeaab53e3b7678c9ce14af5786", "revision": "662bfd0841d0194bfe0a700d54226bb96eac574d",
"branch": "master" "branch": "master"
}, },
{ {

View file

@ -19,7 +19,6 @@
- [2. Analyse the debug output](#2-analyse-the-debug-output) - [2. Analyse the debug output](#2-analyse-the-debug-output)
- [3. Report an issue.](#3-report-an-issue) - [3. Report an issue.](#3-report-an-issue)
- [How do I filter issues between two git refs?](#how-do-i-filter-issues-between-two-git-refs) - [How do I filter issues between two git refs?](#how-do-i-filter-issues-between-two-git-refs)
- [Details](#details)
- [Checkstyle XML format](#checkstyle-xml-format) - [Checkstyle XML format](#checkstyle-xml-format)
<!-- /MarkdownTOC --> <!-- /MarkdownTOC -->
@ -57,12 +56,13 @@ It is intended for use with editor/IDE integration.
- [go vet](https://golang.org/cmd/vet/) - Reports potential errors that otherwise compile. - [go vet](https://golang.org/cmd/vet/) - Reports potential errors that otherwise compile.
- [go tool vet --shadow](https://golang.org/cmd/vet/#hdr-Shadowed_variables) - Reports variables that may have been unintentionally shadowed. - [go tool vet --shadow](https://golang.org/cmd/vet/#hdr-Shadowed_variables) - Reports variables that may have been unintentionally shadowed.
- [gotype](https://golang.org/x/tools/cmd/gotype) - Syntactic and semantic analysis similar to the Go compiler. - [gotype](https://golang.org/x/tools/cmd/gotype) - Syntactic and semantic analysis similar to the Go compiler.
- [gotype -x](https://golang.org/x/tools/cmd/gotype) - Syntactic and semantic analysis in external test packages (similar to the Go compiler).
- [deadcode](https://github.com/tsenart/deadcode) - Finds unused code. - [deadcode](https://github.com/tsenart/deadcode) - Finds unused code.
- [gocyclo](https://github.com/alecthomas/gocyclo) - Computes the cyclomatic complexity of functions. - [gocyclo](https://github.com/alecthomas/gocyclo) - Computes the cyclomatic complexity of functions.
- [golint](https://github.com/golang/lint) - Google's (mostly stylistic) linter. - [golint](https://github.com/golang/lint) - Google's (mostly stylistic) linter.
- [varcheck](https://github.com/opennota/check) - Find unused global variables and constants. - [varcheck](https://github.com/opennota/check) - Find unused global variables and constants.
- [structcheck](https://github.com/opennota/check) - Find unused struct fields. - [structcheck](https://github.com/opennota/check) - Find unused struct fields.
- [aligncheck](https://github.com/opennota/check) - Warn about un-optimally aligned structures. - [maligned](https://github.com/mdempsky/maligned) - Detect structs that would take less memory if their fields were sorted.
- [errcheck](https://github.com/kisielk/errcheck) - Check that error return values are used. - [errcheck](https://github.com/kisielk/errcheck) - Check that error return values are used.
- [megacheck](https://github.com/dominikh/go-tools/tree/master/cmd/megacheck) - Run staticcheck, gosimple and unused, sharing work. - [megacheck](https://github.com/dominikh/go-tools/tree/master/cmd/megacheck) - Run staticcheck, gosimple and unused, sharing work.
- [dupl](https://github.com/mibk/dupl) - Reports potentially duplicated code. - [dupl](https://github.com/mibk/dupl) - Reports potentially duplicated code.
@ -81,6 +81,7 @@ Disabled by default (enable with `--enable=<linter>`):
- [gosimple](https://github.com/dominikh/go-tools/tree/master/cmd/gosimple) - Report simplifications in code. - [gosimple](https://github.com/dominikh/go-tools/tree/master/cmd/gosimple) - Report simplifications in code.
- [lll](https://github.com/walle/lll) - Report long lines (see `--line-length=N`). - [lll](https://github.com/walle/lll) - Report long lines (see `--line-length=N`).
- [misspell](https://github.com/client9/misspell) - Finds commonly misspelled English words. - [misspell](https://github.com/client9/misspell) - Finds commonly misspelled English words.
- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns.
- [unparam](https://github.com/mvdan/unparam) - Find unused function parameters. - [unparam](https://github.com/mvdan/unparam) - Find unused function parameters.
- [unused](https://github.com/dominikh/go-tools/tree/master/cmd/unused) - Find unused variables. - [unused](https://github.com/dominikh/go-tools/tree/master/cmd/unused) - Find unused variables.
- [safesql](https://github.com/stripe/safesql) - Finds potential SQL injection vulnerabilities. - [safesql](https://github.com/stripe/safesql) - Finds potential SQL injection vulnerabilities.
@ -91,14 +92,15 @@ Additional linters can be added through the command line with `--linter=NAME:COM
## Configuration file ## Configuration file
gometalinter now supports a JSON configuration file which can be loaded via gometalinter now supports a JSON configuration file which can be loaded via
`--config=<file>`. The format of this file is determined by the Config struct `--config=<file>`. The format of this file is determined by the `Config` struct
in `config.go`. in [config.go](https://github.com/alecthomas/gometalinter/blob/master/config.go).
The configuration file mostly corresponds to command-line flags, with the following exceptions: The configuration file mostly corresponds to command-line flags, with the following exceptions:
- Linters defined in the configuration file will overlay existing definitions, not replace them. - Linters defined in the configuration file will overlay existing definitions, not replace them.
- "Enable" defines the exact set of linters that will be enabled (default - "Enable" defines the exact set of linters that will be enabled (default
linters are disabled). linters are disabled). `--help` displays the list of default linters with the exact names
you must use.
Here is an example configuration file: Here is an example configuration file:
@ -108,6 +110,34 @@ Here is an example configuration file:
} }
``` ```
### Adding Custom linters
Linters can be added and customized from the config file using the `Linters` field.
Linters supports the following fields:
* `Command` - the path to the linter binary and any default arguments
* `Pattern` - a regular expression used to parse the linter output
* `IsFast` - if the linter should be run when the `--fast` flag is used
* `PartitionStrategy` - how paths args should be passed to the linter command:
* `directories` - call the linter once with a list of all the directories
* `files` - call the linter once with a list of all the files
* `packages` - call the linter once with a list of all the package paths
* `files-by-package` - call the linter once per package with a list of the
files in the package.
* `single-directory` - call the linter once per directory
The config for default linters can be overridden by using the name of the
linter.
Additional linters can be configured via the command line using the format
`NAME:COMMAND:PATTERN`.
Example:
```
$ gometalinter --linter='vet:go tool vet -printfuncs=Infof,Debugf,Warningf,Errorf:PATH:LINE:MESSAGE' .
```
## Installing ## Installing
There are two options for installing gometalinter. There are two options for installing gometalinter.
@ -171,7 +201,8 @@ Install all known linters:
$ gometalinter --install $ gometalinter --install
Installing: Installing:
structcheck structcheck
aligncheck maligned
nakedret
deadcode deadcode
gocyclo gocyclo
ineffassign ineffassign
@ -308,21 +339,6 @@ gometalinter |& revgrep master # Show issues between master and HEAD (or
gometalinter |& revgrep origin/master # Show issues that haven't been pushed. gometalinter |& revgrep origin/master # Show issues that haven't been pushed.
``` ```
## Details
Additional linters can be configured via the command line:
```
$ gometalinter --linter='vet:go tool vet -printfuncs=Infof,Debugf,Warningf,Errorf:PATH:LINE:MESSAGE' .
stutter.go:21:15:warning: error return value not checked (defer a.Close()) (errcheck)
stutter.go:22:15:warning: error return value not checked (defer a.Close()) (errcheck)
stutter.go:27:6:warning: error return value not checked (doit() // test for errcheck) (errcheck)
stutter.go:9::warning: unused global variable unusedGlobal (varcheck)
stutter.go:13::warning: unused struct field MyStruct.Unused (structcheck)
stutter.go:12:6:warning: exported type MyStruct should have comment or be unexported (golint)
stutter.go:16:6:warning: exported type PublicUndocumented should have comment or be unexported (deadcode)
```
## Checkstyle XML format ## Checkstyle XML format
`gometalinter` supports [checkstyle](http://checkstyle.sourceforge.net/) `gometalinter` supports [checkstyle](http://checkstyle.sourceforge.net/)

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Alex Kohler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,310 @@
package main
/*
This file holds a direct copy of the import path matching code of
https://github.com/golang/go/blob/master/src/cmd/go/main.go. It can be
replaced when https://golang.org/issue/8768 is resolved.
It has been updated to follow upstream changes in a few ways.
*/
import (
"fmt"
"go/build"
"log"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
)
var buildContext = build.Default
var (
goroot = filepath.Clean(runtime.GOROOT())
gorootSrc = filepath.Join(goroot, "src")
)
// importPathsNoDotExpansion returns the import paths to use for the given
// command line, but it does no ... expansion.
func importPathsNoDotExpansion(args []string) []string {
if len(args) == 0 {
return []string{"."}
}
var out []string
for _, a := range args {
// Arguments are supposed to be import paths, but
// as a courtesy to Windows developers, rewrite \ to /
// in command-line arguments. Handles .\... and so on.
if filepath.Separator == '\\' {
a = strings.Replace(a, `\`, `/`, -1)
}
// Put argument in canonical form, but preserve leading ./.
if strings.HasPrefix(a, "./") {
a = "./" + path.Clean(a)
if a == "./." {
a = "."
}
} else {
a = path.Clean(a)
}
if a == "all" || a == "std" {
out = append(out, allPackages(a)...)
continue
}
out = append(out, a)
}
return out
}
// importPaths returns the import paths to use for the given command line.
func importPaths(args []string) []string {
args = importPathsNoDotExpansion(args)
var out []string
for _, a := range args {
if strings.Contains(a, "...") {
if build.IsLocalImport(a) {
out = append(out, allPackagesInFS(a)...)
} else {
out = append(out, allPackages(a)...)
}
continue
}
out = append(out, a)
}
return out
}
// matchPattern(pattern)(name) reports whether
// name matches pattern. Pattern is a limited glob
// pattern in which '...' means 'any string' and there
// is no other special syntax.
func matchPattern(pattern string) func(name string) bool {
re := regexp.QuoteMeta(pattern)
re = strings.Replace(re, `\.\.\.`, `.*`, -1)
// Special case: foo/... matches foo too.
if strings.HasSuffix(re, `/.*`) {
re = re[:len(re)-len(`/.*`)] + `(/.*)?`
}
reg := regexp.MustCompile(`^` + re + `$`)
return func(name string) bool {
return reg.MatchString(name)
}
}
// hasPathPrefix reports whether the path s begins with the
// elements in prefix.
func hasPathPrefix(s, prefix string) bool {
switch {
default:
return false
case len(s) == len(prefix):
return s == prefix
case len(s) > len(prefix):
if prefix != "" && prefix[len(prefix)-1] == '/' {
return strings.HasPrefix(s, prefix)
}
return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
}
}
// treeCanMatchPattern(pattern)(name) reports whether
// name or children of name can possibly match pattern.
// Pattern is the same limited glob accepted by matchPattern.
func treeCanMatchPattern(pattern string) func(name string) bool {
wildCard := false
if i := strings.Index(pattern, "..."); i >= 0 {
wildCard = true
pattern = pattern[:i]
}
return func(name string) bool {
return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
wildCard && strings.HasPrefix(name, pattern)
}
}
// allPackages returns all the packages that can be found
// under the $GOPATH directories and $GOROOT matching pattern.
// The pattern is either "all" (all packages), "std" (standard packages)
// or a path including "...".
func allPackages(pattern string) []string {
pkgs := matchPackages(pattern)
if len(pkgs) == 0 {
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
}
return pkgs
}
func matchPackages(pattern string) []string {
match := func(string) bool { return true }
treeCanMatch := func(string) bool { return true }
if pattern != "all" && pattern != "std" {
match = matchPattern(pattern)
treeCanMatch = treeCanMatchPattern(pattern)
}
have := map[string]bool{
"builtin": true, // ignore pseudo-package that exists only for documentation
}
if !buildContext.CgoEnabled {
have["runtime/cgo"] = true // ignore during walk
}
var pkgs []string
// Commands
cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator)
filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error {
if err != nil || !fi.IsDir() || path == cmd {
return nil
}
name := path[len(cmd):]
if !treeCanMatch(name) {
return filepath.SkipDir
}
// Commands are all in cmd/, not in subdirectories.
if strings.Contains(name, string(filepath.Separator)) {
return filepath.SkipDir
}
// We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
name = "cmd/" + name
if have[name] {
return nil
}
have[name] = true
if !match(name) {
return nil
}
_, err = buildContext.ImportDir(path, 0)
if err != nil {
if _, noGo := err.(*build.NoGoError); !noGo {
log.Print(err)
}
return nil
}
pkgs = append(pkgs, name)
return nil
})
for _, src := range buildContext.SrcDirs() {
if (pattern == "std" || pattern == "cmd") && src != gorootSrc {
continue
}
src = filepath.Clean(src) + string(filepath.Separator)
root := src
if pattern == "cmd" {
root += "cmd" + string(filepath.Separator)
}
filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
if err != nil || !fi.IsDir() || path == src {
return nil
}
// Avoid .foo, _foo, testdata and vendor directory trees.
_, elem := filepath.Split(path)
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" || elem == "vendor" {
return filepath.SkipDir
}
name := filepath.ToSlash(path[len(src):])
if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") {
// The name "std" is only the standard library.
// If the name is cmd, it's the root of the command tree.
return filepath.SkipDir
}
if !treeCanMatch(name) {
return filepath.SkipDir
}
if have[name] {
return nil
}
have[name] = true
if !match(name) {
return nil
}
_, err = buildContext.ImportDir(path, 0)
if err != nil {
if _, noGo := err.(*build.NoGoError); noGo {
return nil
}
}
pkgs = append(pkgs, name)
return nil
})
}
return pkgs
}
// allPackagesInFS is like allPackages but is passed a pattern
// beginning ./ or ../, meaning it should scan the tree rooted
// at the given directory. There are ... in the pattern too.
func allPackagesInFS(pattern string) []string {
pkgs := matchPackagesInFS(pattern)
if len(pkgs) == 0 {
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
}
return pkgs
}
func matchPackagesInFS(pattern string) []string {
// Find directory to begin the scan.
// Could be smarter but this one optimization
// is enough for now, since ... is usually at the
// end of a path.
i := strings.Index(pattern, "...")
dir, _ := path.Split(pattern[:i])
// pattern begins with ./ or ../.
// path.Clean will discard the ./ but not the ../.
// We need to preserve the ./ for pattern matching
// and in the returned import paths.
prefix := ""
if strings.HasPrefix(pattern, "./") {
prefix = "./"
}
match := matchPattern(pattern)
var pkgs []string
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil || !fi.IsDir() {
return nil
}
if path == dir {
// filepath.Walk starts at dir and recurses. For the recursive case,
// the path is the result of filepath.Join, which calls filepath.Clean.
// The initial case is not Cleaned, though, so we do this explicitly.
//
// This converts a path like "./io/" to "io". Without this step, running
// "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io
// package, because prepending the prefix "./" to the unclean path would
// result in "././io", and match("././io") returns false.
path = filepath.Clean(path)
}
// Avoid .foo, _foo, testdata and vendor directory trees, but do not avoid "." or "..".
_, elem := filepath.Split(path)
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
if dot || strings.HasPrefix(elem, "_") || elem == "testdata" || elem == "vendor" {
return filepath.SkipDir
}
name := prefix + filepath.ToSlash(path)
if !match(name) {
return nil
}
if _, err = build.ImportDir(path, 0); err != nil {
if _, noGo := err.(*build.NoGoError); !noGo {
log.Print(err)
}
return nil
}
pkgs = append(pkgs, name)
return nil
})
return pkgs
}

View file

@ -0,0 +1,213 @@
package main
import (
"errors"
"flag"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"strings"
)
const (
pwd = "./"
)
func init() {
//TODO allow build tags
build.Default.UseAllFiles = true
}
func usage() {
log.Printf("Usage of %s:\n", os.Args[0])
log.Printf("\nnakedret [flags] # runs on package in current directory\n")
log.Printf("\nnakedret [flags] [packages]\n")
log.Printf("Flags:\n")
flag.PrintDefaults()
}
type returnsVisitor struct {
f *token.FileSet
maxLength uint
}
func main() {
// Remove log timestamp
log.SetFlags(0)
maxLength := flag.Uint("l", 5, "maximum number of lines for a naked return function")
flag.Usage = usage
flag.Parse()
if err := checkNakedReturns(flag.Args(), maxLength); err != nil {
log.Println(err)
}
}
func checkNakedReturns(args []string, maxLength *uint) error {
fset := token.NewFileSet()
files, err := parseInput(args, fset)
if err != nil {
return fmt.Errorf("could not parse input %v", err)
}
if maxLength == nil {
return errors.New("max length nil")
}
retVis := &returnsVisitor{
f: fset,
maxLength: *maxLength,
}
for _, f := range files {
ast.Walk(retVis, f)
}
return nil
}
func parseInput(args []string, fset *token.FileSet) ([]*ast.File, error) {
var directoryList []string
var fileMode bool
files := make([]*ast.File, 0)
if len(args) == 0 {
directoryList = append(directoryList, pwd)
} else {
for _, arg := range args {
if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-len("/...")]) {
for _, dirname := range allPackagesInFS(arg) {
directoryList = append(directoryList, dirname)
}
} else if isDir(arg) {
directoryList = append(directoryList, arg)
} else if exists(arg) {
if strings.HasSuffix(arg, ".go") {
fileMode = true
f, err := parser.ParseFile(fset, arg, nil, 0)
if err != nil {
return nil, err
}
files = append(files, f)
} else {
return nil, fmt.Errorf("invalid file %v specified", arg)
}
} else {
//TODO clean this up a bit
imPaths := importPaths([]string{arg})
for _, importPath := range imPaths {
pkg, err := build.Import(importPath, ".", 0)
if err != nil {
return nil, err
}
var stringFiles []string
stringFiles = append(stringFiles, pkg.GoFiles...)
// files = append(files, pkg.CgoFiles...)
stringFiles = append(stringFiles, pkg.TestGoFiles...)
if pkg.Dir != "." {
for i, f := range stringFiles {
stringFiles[i] = filepath.Join(pkg.Dir, f)
}
}
fileMode = true
for _, stringFile := range stringFiles {
f, err := parser.ParseFile(fset, stringFile, nil, 0)
if err != nil {
return nil, err
}
files = append(files, f)
}
}
}
}
}
// if we're not in file mode, then we need to grab each and every package in each directory
// we can to grab all the files
if !fileMode {
for _, fpath := range directoryList {
pkgs, err := parser.ParseDir(fset, fpath, nil, 0)
if err != nil {
return nil, err
}
for _, pkg := range pkgs {
for _, f := range pkg.Files {
files = append(files, f)
}
}
}
}
return files, nil
}
func isDir(filename string) bool {
fi, err := os.Stat(filename)
return err == nil && fi.IsDir()
}
func exists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
func (v *returnsVisitor) Visit(node ast.Node) ast.Visitor {
var namedReturns []*ast.Ident
funcDecl, ok := node.(*ast.FuncDecl)
if !ok {
return v
}
var functionLineLength int
// We've found a function
if funcDecl.Type != nil && funcDecl.Type.Results != nil {
for _, field := range funcDecl.Type.Results.List {
for _, ident := range field.Names {
if ident != nil {
namedReturns = append(namedReturns, ident)
}
}
}
file := v.f.File(funcDecl.Pos())
functionLineLength = file.Position(funcDecl.End()).Line - file.Position(funcDecl.Pos()).Line
}
if len(namedReturns) > 0 && funcDecl.Body != nil {
// Scan the body for usage of the named returns
for _, stmt := range funcDecl.Body.List {
switch s := stmt.(type) {
case *ast.ReturnStmt:
if len(s.Results) == 0 {
file := v.f.File(s.Pos())
if file != nil && uint(functionLineLength) > v.maxLength {
if funcDecl.Name != nil {
log.Printf("%v:%v %v naked returns on %v line function \n", file.Name(), file.Position(s.Pos()).Line, funcDecl.Name.Name, functionLineLength)
}
}
continue
}
default:
}
}
}
return v
}

View file

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,682 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Identify mismatches between assembly files and Go func declarations.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/token"
"regexp"
"strconv"
"strings"
)
// 'kind' is a kind of assembly variable.
// The kinds 1, 2, 4, 8 stand for values of that size.
type asmKind int
// These special kinds are not valid sizes.
const (
asmString asmKind = 100 + iota
asmSlice
asmInterface
asmEmptyInterface
)
// An asmArch describes assembly parameters for an architecture
type asmArch struct {
name string
ptrSize int
intSize int
maxAlign int
bigEndian bool
stack string
lr bool
}
// An asmFunc describes the expected variables for a function on a given architecture.
type asmFunc struct {
arch *asmArch
size int // size of all arguments
vars map[string]*asmVar
varByOffset map[int]*asmVar
}
// An asmVar describes a single assembly variable.
type asmVar struct {
name string
kind asmKind
typ string
off int
size int
inner []*asmVar
}
var (
asmArch386 = asmArch{"386", 4, 4, 4, false, "SP", false}
asmArchArm = asmArch{"arm", 4, 4, 4, false, "R13", true}
asmArchArm64 = asmArch{"arm64", 8, 8, 8, false, "RSP", true}
asmArchAmd64 = asmArch{"amd64", 8, 8, 8, false, "SP", false}
asmArchAmd64p32 = asmArch{"amd64p32", 4, 4, 8, false, "SP", false}
asmArchMips64 = asmArch{"mips64", 8, 8, 8, true, "R29", true}
asmArchMips64LE = asmArch{"mips64", 8, 8, 8, false, "R29", true}
asmArchPpc64 = asmArch{"ppc64", 8, 8, 8, true, "R1", true}
asmArchPpc64LE = asmArch{"ppc64le", 8, 8, 8, false, "R1", true}
arches = []*asmArch{
&asmArch386,
&asmArchArm,
&asmArchArm64,
&asmArchAmd64,
&asmArchAmd64p32,
&asmArchMips64,
&asmArchMips64LE,
&asmArchPpc64,
&asmArchPpc64LE,
}
)
var (
re = regexp.MustCompile
asmPlusBuild = re(`//\s+\+build\s+([^\n]+)`)
asmTEXT = re(`\bTEXT\b.*·([^\(]+)\(SB\)(?:\s*,\s*([0-9A-Z|+]+))?(?:\s*,\s*\$(-?[0-9]+)(?:-([0-9]+))?)?`)
asmDATA = re(`\b(DATA|GLOBL)\b`)
asmNamedFP = re(`([a-zA-Z0-9_\xFF-\x{10FFFF}]+)(?:\+([0-9]+))\(FP\)`)
asmUnnamedFP = re(`[^+\-0-9](([0-9]+)\(FP\))`)
asmSP = re(`[^+\-0-9](([0-9]+)\(([A-Z0-9]+)\))`)
asmOpcode = re(`^\s*(?:[A-Z0-9a-z_]+:)?\s*([A-Z]+)\s*([^,]*)(?:,\s*(.*))?`)
ppc64Suff = re(`([BHWD])(ZU|Z|U|BR)?$`)
)
func asmCheck(pkg *Package) {
if !vet("asmdecl") {
return
}
// No work if no assembly files.
if !pkg.hasFileWithSuffix(".s") {
return
}
// Gather declarations. knownFunc[name][arch] is func description.
knownFunc := make(map[string]map[string]*asmFunc)
for _, f := range pkg.files {
if f.file != nil {
for _, decl := range f.file.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil {
knownFunc[decl.Name.Name] = f.asmParseDecl(decl)
}
}
}
}
Files:
for _, f := range pkg.files {
if !strings.HasSuffix(f.name, ".s") {
continue
}
Println("Checking file", f.name)
// Determine architecture from file name if possible.
var arch string
var archDef *asmArch
for _, a := range arches {
if strings.HasSuffix(f.name, "_"+a.name+".s") {
arch = a.name
archDef = a
break
}
}
lines := strings.SplitAfter(string(f.content), "\n")
var (
fn *asmFunc
fnName string
localSize, argSize int
wroteSP bool
haveRetArg bool
retLine []int
)
flushRet := func() {
if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 {
v := fn.vars["ret"]
for _, line := range retLine {
f.Badf(token.NoPos, "%s:%d: [%s] %s: RET without writing to %d-byte ret+%d(FP)", f.name, line, arch, fnName, v.size, v.off)
}
}
retLine = nil
}
for lineno, line := range lines {
lineno++
badf := func(format string, args ...interface{}) {
f.Badf(token.NoPos, "%s:%d: [%s] %s: %s", f.name, lineno, arch, fnName, fmt.Sprintf(format, args...))
}
if arch == "" {
// Determine architecture from +build line if possible.
if m := asmPlusBuild.FindStringSubmatch(line); m != nil {
Fields:
for _, fld := range strings.Fields(m[1]) {
for _, a := range arches {
if a.name == fld {
arch = a.name
archDef = a
break Fields
}
}
}
}
}
if m := asmTEXT.FindStringSubmatch(line); m != nil {
flushRet()
if arch == "" {
f.Warnf(token.NoPos, "%s: cannot determine architecture for assembly file", f.name)
continue Files
}
fnName = m[1]
fn = knownFunc[m[1]][arch]
if fn != nil {
size, _ := strconv.Atoi(m[4])
if size != fn.size && (m[2] != "7" && !strings.Contains(m[2], "NOSPLIT") || size != 0) {
badf("wrong argument size %d; expected $...-%d", size, fn.size)
}
}
localSize, _ = strconv.Atoi(m[3])
localSize += archDef.intSize
if archDef.lr {
// Account for caller's saved LR
localSize += archDef.intSize
}
argSize, _ = strconv.Atoi(m[4])
if fn == nil && !strings.Contains(fnName, "<>") {
badf("function %s missing Go declaration", fnName)
}
wroteSP = false
haveRetArg = false
continue
} else if strings.Contains(line, "TEXT") && strings.Contains(line, "SB") {
// function, but not visible from Go (didn't match asmTEXT), so stop checking
flushRet()
fn = nil
fnName = ""
continue
}
if strings.Contains(line, "RET") {
retLine = append(retLine, lineno)
}
if fnName == "" {
continue
}
if asmDATA.FindStringSubmatch(line) != nil {
fn = nil
}
if archDef == nil {
continue
}
if strings.Contains(line, ", "+archDef.stack) || strings.Contains(line, ",\t"+archDef.stack) {
wroteSP = true
continue
}
for _, m := range asmSP.FindAllStringSubmatch(line, -1) {
if m[3] != archDef.stack || wroteSP {
continue
}
off := 0
if m[1] != "" {
off, _ = strconv.Atoi(m[2])
}
if off >= localSize {
if fn != nil {
v := fn.varByOffset[off-localSize]
if v != nil {
badf("%s should be %s+%d(FP)", m[1], v.name, off-localSize)
continue
}
}
if off >= localSize+argSize {
badf("use of %s points beyond argument frame", m[1])
continue
}
badf("use of %s to access argument frame", m[1])
}
}
if fn == nil {
continue
}
for _, m := range asmUnnamedFP.FindAllStringSubmatch(line, -1) {
off, _ := strconv.Atoi(m[2])
v := fn.varByOffset[off]
if v != nil {
badf("use of unnamed argument %s; offset %d is %s+%d(FP)", m[1], off, v.name, v.off)
} else {
badf("use of unnamed argument %s", m[1])
}
}
for _, m := range asmNamedFP.FindAllStringSubmatch(line, -1) {
name := m[1]
off := 0
if m[2] != "" {
off, _ = strconv.Atoi(m[2])
}
if name == "ret" || strings.HasPrefix(name, "ret_") {
haveRetArg = true
}
v := fn.vars[name]
if v == nil {
// Allow argframe+0(FP).
if name == "argframe" && off == 0 {
continue
}
v = fn.varByOffset[off]
if v != nil {
badf("unknown variable %s; offset %d is %s+%d(FP)", name, off, v.name, v.off)
} else {
badf("unknown variable %s", name)
}
continue
}
asmCheckVar(badf, fn, line, m[0], off, v)
}
}
flushRet()
}
}
// asmParseDecl parses a function decl for expected assembly variables.
func (f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc {
var (
arch *asmArch
fn *asmFunc
offset int
failed bool
)
addVar := func(outer string, v asmVar) {
if vo := fn.vars[outer]; vo != nil {
vo.inner = append(vo.inner, &v)
}
fn.vars[v.name] = &v
for i := 0; i < v.size; i++ {
fn.varByOffset[v.off+i] = &v
}
}
addParams := func(list []*ast.Field) {
for i, fld := range list {
// Determine alignment, size, and kind of type in declaration.
var align, size int
var kind asmKind
names := fld.Names
typ := f.gofmt(fld.Type)
switch t := fld.Type.(type) {
default:
switch typ {
default:
f.Warnf(fld.Type.Pos(), "unknown assembly argument type %s", typ)
failed = true
return
case "int8", "uint8", "byte", "bool":
size = 1
case "int16", "uint16":
size = 2
case "int32", "uint32", "float32":
size = 4
case "int64", "uint64", "float64":
align = arch.maxAlign
size = 8
case "int", "uint":
size = arch.intSize
case "uintptr", "iword", "Word", "Errno", "unsafe.Pointer":
size = arch.ptrSize
case "string", "ErrorString":
size = arch.ptrSize * 2
align = arch.ptrSize
kind = asmString
}
case *ast.ChanType, *ast.FuncType, *ast.MapType, *ast.StarExpr:
size = arch.ptrSize
case *ast.InterfaceType:
align = arch.ptrSize
size = 2 * arch.ptrSize
if len(t.Methods.List) > 0 {
kind = asmInterface
} else {
kind = asmEmptyInterface
}
case *ast.ArrayType:
if t.Len == nil {
size = arch.ptrSize + 2*arch.intSize
align = arch.ptrSize
kind = asmSlice
break
}
f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ)
failed = true
case *ast.StructType:
f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ)
failed = true
}
if align == 0 {
align = size
}
if kind == 0 {
kind = asmKind(size)
}
offset += -offset & (align - 1)
// Create variable for each name being declared with this type.
if len(names) == 0 {
name := "unnamed"
if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 && &list[0] == &decl.Type.Results.List[0] && i == 0 {
// Assume assembly will refer to single unnamed result as r.
name = "ret"
}
names = []*ast.Ident{{Name: name}}
}
for _, id := range names {
name := id.Name
addVar("", asmVar{
name: name,
kind: kind,
typ: typ,
off: offset,
size: size,
})
switch kind {
case 8:
if arch.ptrSize == 4 {
w1, w2 := "lo", "hi"
if arch.bigEndian {
w1, w2 = w2, w1
}
addVar(name, asmVar{
name: name + "_" + w1,
kind: 4,
typ: "half " + typ,
off: offset,
size: 4,
})
addVar(name, asmVar{
name: name + "_" + w2,
kind: 4,
typ: "half " + typ,
off: offset + 4,
size: 4,
})
}
case asmEmptyInterface:
addVar(name, asmVar{
name: name + "_type",
kind: asmKind(arch.ptrSize),
typ: "interface type",
off: offset,
size: arch.ptrSize,
})
addVar(name, asmVar{
name: name + "_data",
kind: asmKind(arch.ptrSize),
typ: "interface data",
off: offset + arch.ptrSize,
size: arch.ptrSize,
})
case asmInterface:
addVar(name, asmVar{
name: name + "_itable",
kind: asmKind(arch.ptrSize),
typ: "interface itable",
off: offset,
size: arch.ptrSize,
})
addVar(name, asmVar{
name: name + "_data",
kind: asmKind(arch.ptrSize),
typ: "interface data",
off: offset + arch.ptrSize,
size: arch.ptrSize,
})
case asmSlice:
addVar(name, asmVar{
name: name + "_base",
kind: asmKind(arch.ptrSize),
typ: "slice base",
off: offset,
size: arch.ptrSize,
})
addVar(name, asmVar{
name: name + "_len",
kind: asmKind(arch.intSize),
typ: "slice len",
off: offset + arch.ptrSize,
size: arch.intSize,
})
addVar(name, asmVar{
name: name + "_cap",
kind: asmKind(arch.intSize),
typ: "slice cap",
off: offset + arch.ptrSize + arch.intSize,
size: arch.intSize,
})
case asmString:
addVar(name, asmVar{
name: name + "_base",
kind: asmKind(arch.ptrSize),
typ: "string base",
off: offset,
size: arch.ptrSize,
})
addVar(name, asmVar{
name: name + "_len",
kind: asmKind(arch.intSize),
typ: "string len",
off: offset + arch.ptrSize,
size: arch.intSize,
})
}
offset += size
}
}
}
m := make(map[string]*asmFunc)
for _, arch = range arches {
fn = &asmFunc{
arch: arch,
vars: make(map[string]*asmVar),
varByOffset: make(map[int]*asmVar),
}
offset = 0
addParams(decl.Type.Params.List)
if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 {
offset += -offset & (arch.maxAlign - 1)
addParams(decl.Type.Results.List)
}
fn.size = offset
m[arch.name] = fn
}
if failed {
return nil
}
return m
}
// asmCheckVar checks a single variable reference.
func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar) {
m := asmOpcode.FindStringSubmatch(line)
if m == nil {
if !strings.HasPrefix(strings.TrimSpace(line), "//") {
badf("cannot find assembly opcode")
}
return
}
// Determine operand sizes from instruction.
// Typically the suffix suffices, but there are exceptions.
var src, dst, kind asmKind
op := m[1]
switch fn.arch.name + "." + op {
case "386.FMOVLP":
src, dst = 8, 4
case "arm.MOVD":
src = 8
case "arm.MOVW":
src = 4
case "arm.MOVH", "arm.MOVHU":
src = 2
case "arm.MOVB", "arm.MOVBU":
src = 1
// LEA* opcodes don't really read the second arg.
// They just take the address of it.
case "386.LEAL":
dst = 4
case "amd64.LEAQ":
dst = 8
case "amd64p32.LEAL":
dst = 4
default:
switch fn.arch.name {
case "386", "amd64":
if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "D") || strings.HasSuffix(op, "DP")) {
// FMOVDP, FXCHD, etc
src = 8
break
}
if strings.HasPrefix(op, "P") && strings.HasSuffix(op, "RD") {
// PINSRD, PEXTRD, etc
src = 4
break
}
if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "F") || strings.HasSuffix(op, "FP")) {
// FMOVFP, FXCHF, etc
src = 4
break
}
if strings.HasSuffix(op, "SD") {
// MOVSD, SQRTSD, etc
src = 8
break
}
if strings.HasSuffix(op, "SS") {
// MOVSS, SQRTSS, etc
src = 4
break
}
if strings.HasPrefix(op, "SET") {
// SETEQ, etc
src = 1
break
}
switch op[len(op)-1] {
case 'B':
src = 1
case 'W':
src = 2
case 'L':
src = 4
case 'D', 'Q':
src = 8
}
case "ppc64", "ppc64le":
// Strip standard suffixes to reveal size letter.
m := ppc64Suff.FindStringSubmatch(op)
if m != nil {
switch m[1][0] {
case 'B':
src = 1
case 'H':
src = 2
case 'W':
src = 4
case 'D':
src = 8
}
}
case "mips64", "mips64le":
switch op {
case "MOVB", "MOVBU":
src = 1
case "MOVH", "MOVHU":
src = 2
case "MOVW", "MOVWU", "MOVF":
src = 4
case "MOVV", "MOVD":
src = 8
}
}
}
if dst == 0 {
dst = src
}
// Determine whether the match we're holding
// is the first or second argument.
if strings.Index(line, expr) > strings.Index(line, ",") {
kind = dst
} else {
kind = src
}
vk := v.kind
vt := v.typ
switch vk {
case asmInterface, asmEmptyInterface, asmString, asmSlice:
// allow reference to first word (pointer)
vk = v.inner[0].kind
vt = v.inner[0].typ
}
if off != v.off {
var inner bytes.Buffer
for i, vi := range v.inner {
if len(v.inner) > 1 {
fmt.Fprintf(&inner, ",")
}
fmt.Fprintf(&inner, " ")
if i == len(v.inner)-1 {
fmt.Fprintf(&inner, "or ")
}
fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
}
badf("invalid offset %s; expected %s+%d(FP)%s", expr, v.name, v.off, inner.String())
return
}
if kind != 0 && kind != vk {
var inner bytes.Buffer
if len(v.inner) > 0 {
fmt.Fprintf(&inner, " containing")
for i, vi := range v.inner {
if i > 0 && len(v.inner) > 2 {
fmt.Fprintf(&inner, ",")
}
fmt.Fprintf(&inner, " ")
if i > 0 && i == len(v.inner)-1 {
fmt.Fprintf(&inner, "and ")
}
fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
}
}
badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vk, inner.String())
}
}

View file

@ -0,0 +1,49 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
This file contains the code to check for useless assignments.
*/
package main
import (
"go/ast"
"go/token"
"reflect"
)
func init() {
register("assign",
"check for useless assignments",
checkAssignStmt,
assignStmt)
}
// TODO: should also check for assignments to struct fields inside methods
// that are on T instead of *T.
// checkAssignStmt checks for assignments of the form "<expr> = <expr>".
// These are almost always useless, and even when they aren't they are usually a mistake.
func checkAssignStmt(f *File, node ast.Node) {
stmt := node.(*ast.AssignStmt)
if stmt.Tok != token.ASSIGN {
return // ignore :=
}
if len(stmt.Lhs) != len(stmt.Rhs) {
// If LHS and RHS have different cardinality, they can't be the same.
return
}
for i, lhs := range stmt.Lhs {
rhs := stmt.Rhs[i]
if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) {
continue // short-circuit the heavy-weight gofmt check
}
le := f.gofmt(lhs)
re := f.gofmt(rhs)
if le == re {
f.Badf(stmt.Pos(), "self-assignment of %s to %s", re, le)
}
}
}

View file

@ -0,0 +1,69 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"go/ast"
"go/token"
)
func init() {
register("atomic",
"check for common mistaken usages of the sync/atomic package",
checkAtomicAssignment,
assignStmt)
}
// checkAtomicAssignment walks the assignment statement checking for common
// mistaken usage of atomic package, such as: x = atomic.AddUint64(&x, 1)
func checkAtomicAssignment(f *File, node ast.Node) {
n := node.(*ast.AssignStmt)
if len(n.Lhs) != len(n.Rhs) {
return
}
if len(n.Lhs) == 1 && n.Tok == token.DEFINE {
return
}
for i, right := range n.Rhs {
call, ok := right.(*ast.CallExpr)
if !ok {
continue
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
pkg, ok := sel.X.(*ast.Ident)
if !ok || pkg.Name != "atomic" {
continue
}
switch sel.Sel.Name {
case "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr":
f.checkAtomicAddAssignment(n.Lhs[i], call)
}
}
}
// checkAtomicAddAssignment walks the atomic.Add* method calls checking for assigning the return value
// to the same variable being used in the operation
func (f *File) checkAtomicAddAssignment(left ast.Expr, call *ast.CallExpr) {
if len(call.Args) != 2 {
return
}
arg := call.Args[0]
broken := false
if uarg, ok := arg.(*ast.UnaryExpr); ok && uarg.Op == token.AND {
broken = f.gofmt(left) == f.gofmt(uarg.X)
} else if star, ok := left.(*ast.StarExpr); ok {
broken = f.gofmt(star.X) == f.gofmt(arg)
}
if broken {
f.Bad(left.Pos(), "direct assignment to atomic value")
}
}

View file

@ -0,0 +1,186 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains boolean condition tests.
package main
import (
"go/ast"
"go/token"
)
func init() {
register("bool",
"check for mistakes involving boolean operators",
checkBool,
binaryExpr)
}
func checkBool(f *File, n ast.Node) {
e := n.(*ast.BinaryExpr)
var op boolOp
switch e.Op {
case token.LOR:
op = or
case token.LAND:
op = and
default:
return
}
comm := op.commutativeSets(e)
for _, exprs := range comm {
op.checkRedundant(f, exprs)
op.checkSuspect(f, exprs)
}
}
type boolOp struct {
name string
tok token.Token // token corresponding to this operator
badEq token.Token // token corresponding to the equality test that should not be used with this operator
}
var (
or = boolOp{"or", token.LOR, token.NEQ}
and = boolOp{"and", token.LAND, token.EQL}
)
// commutativeSets returns all side effect free sets of
// expressions in e that are connected by op.
// For example, given 'a || b || f() || c || d' with the or op,
// commutativeSets returns {{b, a}, {d, c}}.
func (op boolOp) commutativeSets(e *ast.BinaryExpr) [][]ast.Expr {
exprs := op.split(e)
// Partition the slice of expressions into commutative sets.
i := 0
var sets [][]ast.Expr
for j := 0; j <= len(exprs); j++ {
if j == len(exprs) || hasSideEffects(exprs[j]) {
if i < j {
sets = append(sets, exprs[i:j])
}
i = j + 1
}
}
return sets
}
// checkRedundant checks for expressions of the form
// e && e
// e || e
// Exprs must contain only side effect free expressions.
func (op boolOp) checkRedundant(f *File, exprs []ast.Expr) {
seen := make(map[string]bool)
for _, e := range exprs {
efmt := f.gofmt(e)
if seen[efmt] {
f.Badf(e.Pos(), "redundant %s: %s %s %s", op.name, efmt, op.tok, efmt)
} else {
seen[efmt] = true
}
}
}
// checkSuspect checks for expressions of the form
// x != c1 || x != c2
// x == c1 && x == c2
// where c1 and c2 are constant expressions.
// If c1 and c2 are the same then it's redundant;
// if c1 and c2 are different then it's always true or always false.
// Exprs must contain only side effect free expressions.
func (op boolOp) checkSuspect(f *File, exprs []ast.Expr) {
// seen maps from expressions 'x' to equality expressions 'x != c'.
seen := make(map[string]string)
for _, e := range exprs {
bin, ok := e.(*ast.BinaryExpr)
if !ok || bin.Op != op.badEq {
continue
}
// In order to avoid false positives, restrict to cases
// in which one of the operands is constant. We're then
// interested in the other operand.
// In the rare case in which both operands are constant
// (e.g. runtime.GOOS and "windows"), we'll only catch
// mistakes if the LHS is repeated, which is how most
// code is written.
var x ast.Expr
switch {
case f.pkg.types[bin.Y].Value != nil:
x = bin.X
case f.pkg.types[bin.X].Value != nil:
x = bin.Y
default:
continue
}
// e is of the form 'x != c' or 'x == c'.
xfmt := f.gofmt(x)
efmt := f.gofmt(e)
if prev, found := seen[xfmt]; found {
// checkRedundant handles the case in which efmt == prev.
if efmt != prev {
f.Badf(e.Pos(), "suspect %s: %s %s %s", op.name, efmt, op.tok, prev)
}
} else {
seen[xfmt] = efmt
}
}
}
// hasSideEffects reports whether evaluation of e has side effects.
func hasSideEffects(e ast.Expr) bool {
safe := true
ast.Inspect(e, func(node ast.Node) bool {
switch n := node.(type) {
// Using CallExpr here will catch conversions
// as well as function and method invocations.
// We'll live with the false negatives for now.
case *ast.CallExpr:
safe = false
return false
case *ast.UnaryExpr:
if n.Op == token.ARROW {
safe = false
return false
}
}
return true
})
return !safe
}
// split returns a slice of all subexpressions in e that are connected by op.
// For example, given 'a || (b || c) || d' with the or op,
// split returns []{d, c, b, a}.
func (op boolOp) split(e ast.Expr) (exprs []ast.Expr) {
for {
e = unparen(e)
if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok {
exprs = append(exprs, op.split(b.Y)...)
e = b.X
} else {
exprs = append(exprs, e)
break
}
}
return
}
// unparen returns e with any enclosing parentheses stripped.
func unparen(e ast.Expr) ast.Expr {
for {
p, ok := e.(*ast.ParenExpr)
if !ok {
return e
}
e = p.X
}
}

View file

@ -0,0 +1,91 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"fmt"
"os"
"strings"
"unicode"
)
var (
nl = []byte("\n")
slashSlash = []byte("//")
plusBuild = []byte("+build")
)
// checkBuildTag checks that build tags are in the correct location and well-formed.
func checkBuildTag(name string, data []byte) {
if !vet("buildtags") {
return
}
lines := bytes.SplitAfter(data, nl)
// Determine cutpoint where +build comments are no longer valid.
// They are valid in leading // comments in the file followed by
// a blank line.
var cutoff int
for i, line := range lines {
line = bytes.TrimSpace(line)
if len(line) == 0 {
cutoff = i
continue
}
if bytes.HasPrefix(line, slashSlash) {
continue
}
break
}
for i, line := range lines {
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, slashSlash) {
continue
}
text := bytes.TrimSpace(line[2:])
if bytes.HasPrefix(text, plusBuild) {
fields := bytes.Fields(text)
if !bytes.Equal(fields[0], plusBuild) {
// Comment is something like +buildasdf not +build.
fmt.Fprintf(os.Stderr, "%s:%d: possible malformed +build comment\n", name, i+1)
continue
}
if i >= cutoff {
fmt.Fprintf(os.Stderr, "%s:%d: +build comment must appear before package clause and be followed by a blank line\n", name, i+1)
setExit(1)
continue
}
// Check arguments.
Args:
for _, arg := range fields[1:] {
for _, elem := range strings.Split(string(arg), ",") {
if strings.HasPrefix(elem, "!!") {
fmt.Fprintf(os.Stderr, "%s:%d: invalid double negative in build constraint: %s\n", name, i+1, arg)
setExit(1)
break Args
}
if strings.HasPrefix(elem, "!") {
elem = elem[1:]
}
for _, c := range elem {
if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
fmt.Fprintf(os.Stderr, "%s:%d: invalid non-alphanumeric build constraint: %s\n", name, i+1, arg)
setExit(1)
break Args
}
}
}
}
continue
}
// Comment with +build but not at beginning.
if bytes.Contains(line, plusBuild) && i < cutoff {
fmt.Fprintf(os.Stderr, "%s:%d: possible malformed +build comment\n", name, i+1)
continue
}
}
}

View file

@ -0,0 +1,130 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Check for invalid cgo pointer passing.
// This looks for code that uses cgo to call C code passing values
// whose types are almost always invalid according to the cgo pointer
// sharing rules.
// Specifically, it warns about attempts to pass a Go chan, map, func,
// or slice to C, either directly, or via a pointer, array, or struct.
package main
import (
"go/ast"
"go/token"
"go/types"
)
func init() {
register("cgocall",
"check for types that may not be passed to cgo calls",
checkCgoCall,
callExpr)
}
func checkCgoCall(f *File, node ast.Node) {
x := node.(*ast.CallExpr)
// We are only looking for calls to functions imported from
// the "C" package.
sel, ok := x.Fun.(*ast.SelectorExpr)
if !ok {
return
}
id, ok := sel.X.(*ast.Ident)
if !ok || id.Name != "C" {
return
}
for _, arg := range x.Args {
if !typeOKForCgoCall(cgoBaseType(f, arg)) {
f.Badf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
}
// Check for passing the address of a bad type.
if conv, ok := arg.(*ast.CallExpr); ok && len(conv.Args) == 1 && f.hasBasicType(conv.Fun, types.UnsafePointer) {
arg = conv.Args[0]
}
if u, ok := arg.(*ast.UnaryExpr); ok && u.Op == token.AND {
if !typeOKForCgoCall(cgoBaseType(f, u.X)) {
f.Badf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
}
}
}
}
// cgoBaseType tries to look through type conversions involving
// unsafe.Pointer to find the real type. It converts:
// unsafe.Pointer(x) => x
// *(*unsafe.Pointer)(unsafe.Pointer(&x)) => x
func cgoBaseType(f *File, arg ast.Expr) types.Type {
switch arg := arg.(type) {
case *ast.CallExpr:
if len(arg.Args) == 1 && f.hasBasicType(arg.Fun, types.UnsafePointer) {
return cgoBaseType(f, arg.Args[0])
}
case *ast.StarExpr:
call, ok := arg.X.(*ast.CallExpr)
if !ok || len(call.Args) != 1 {
break
}
// Here arg is *f(v).
t := f.pkg.types[call.Fun].Type
if t == nil {
break
}
ptr, ok := t.Underlying().(*types.Pointer)
if !ok {
break
}
// Here arg is *(*p)(v)
elem, ok := ptr.Elem().Underlying().(*types.Basic)
if !ok || elem.Kind() != types.UnsafePointer {
break
}
// Here arg is *(*unsafe.Pointer)(v)
call, ok = call.Args[0].(*ast.CallExpr)
if !ok || len(call.Args) != 1 {
break
}
// Here arg is *(*unsafe.Pointer)(f(v))
if !f.hasBasicType(call.Fun, types.UnsafePointer) {
break
}
// Here arg is *(*unsafe.Pointer)(unsafe.Pointer(v))
u, ok := call.Args[0].(*ast.UnaryExpr)
if !ok || u.Op != token.AND {
break
}
// Here arg is *(*unsafe.Pointer)(unsafe.Pointer(&v))
return cgoBaseType(f, u.X)
}
return f.pkg.types[arg].Type
}
// typeOKForCgoCall returns true if the type of arg is OK to pass to a
// C function using cgo. This is not true for Go types with embedded
// pointers.
func typeOKForCgoCall(t types.Type) bool {
if t == nil {
return true
}
switch t := t.Underlying().(type) {
case *types.Chan, *types.Map, *types.Signature, *types.Slice:
return false
case *types.Pointer:
return typeOKForCgoCall(t.Elem())
case *types.Array:
return typeOKForCgoCall(t.Elem())
case *types.Struct:
for i := 0; i < t.NumFields(); i++ {
if !typeOKForCgoCall(t.Field(i).Type()) {
return false
}
}
}
return true
}

View file

@ -0,0 +1,82 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains the test for unkeyed struct literals.
package main
import (
"github.com/dnephin/govet/internal/whitelist"
"flag"
"go/ast"
"go/types"
"strings"
)
var compositeWhiteList = flag.Bool("compositewhitelist", true, "use composite white list; for testing only")
func init() {
register("composites",
"check that composite literals used field-keyed elements",
checkUnkeyedLiteral,
compositeLit)
}
// checkUnkeyedLiteral checks if a composite literal is a struct literal with
// unkeyed fields.
func checkUnkeyedLiteral(f *File, node ast.Node) {
cl := node.(*ast.CompositeLit)
typ := f.pkg.types[cl].Type
if typ == nil {
// cannot determine composite literals' type, skip it
return
}
typeName := typ.String()
if *compositeWhiteList && whitelist.UnkeyedLiteral[typeName] {
// skip whitelisted types
return
}
if _, ok := typ.Underlying().(*types.Struct); !ok {
// skip non-struct composite literals
return
}
if isLocalType(f, typeName) {
// allow unkeyed locally defined composite literal
return
}
// check if the CompositeLit contains an unkeyed field
allKeyValue := true
for _, e := range cl.Elts {
if _, ok := e.(*ast.KeyValueExpr); !ok {
allKeyValue = false
break
}
}
if allKeyValue {
// all the composite literal fields are keyed
return
}
f.Badf(cl.Pos(), "%s composite literal uses unkeyed fields", typeName)
}
func isLocalType(f *File, typeName string) bool {
if strings.HasPrefix(typeName, "struct{") {
// struct literals are local types
return true
}
pkgname := f.pkg.path
if strings.HasPrefix(typeName, pkgname+".") {
return true
}
// treat types as local inside test packages with _test name suffix
if strings.HasSuffix(pkgname, "_test") {
pkgname = pkgname[:len(pkgname)-len("_test")]
}
return strings.HasPrefix(typeName, pkgname+".")
}

View file

@ -0,0 +1,239 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains the code to check that locks are not passed by value.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/token"
"go/types"
)
func init() {
register("copylocks",
"check that locks are not passed by value",
checkCopyLocks,
funcDecl, rangeStmt, funcLit, callExpr, assignStmt, genDecl, compositeLit, returnStmt)
}
// checkCopyLocks checks whether node might
// inadvertently copy a lock.
func checkCopyLocks(f *File, node ast.Node) {
switch node := node.(type) {
case *ast.RangeStmt:
checkCopyLocksRange(f, node)
case *ast.FuncDecl:
checkCopyLocksFunc(f, node.Name.Name, node.Recv, node.Type)
case *ast.FuncLit:
checkCopyLocksFunc(f, "func", nil, node.Type)
case *ast.CallExpr:
checkCopyLocksCallExpr(f, node)
case *ast.AssignStmt:
checkCopyLocksAssign(f, node)
case *ast.GenDecl:
checkCopyLocksGenDecl(f, node)
case *ast.CompositeLit:
checkCopyLocksCompositeLit(f, node)
case *ast.ReturnStmt:
checkCopyLocksReturnStmt(f, node)
}
}
// checkCopyLocksAssign checks whether an assignment
// copies a lock.
func checkCopyLocksAssign(f *File, as *ast.AssignStmt) {
for i, x := range as.Rhs {
if path := lockPathRhs(f, x); path != nil {
f.Badf(x.Pos(), "assignment copies lock value to %v: %v", f.gofmt(as.Lhs[i]), path)
}
}
}
// checkCopyLocksGenDecl checks whether lock is copied
// in variable declaration.
func checkCopyLocksGenDecl(f *File, gd *ast.GenDecl) {
if gd.Tok != token.VAR {
return
}
for _, spec := range gd.Specs {
valueSpec := spec.(*ast.ValueSpec)
for i, x := range valueSpec.Values {
if path := lockPathRhs(f, x); path != nil {
f.Badf(x.Pos(), "variable declaration copies lock value to %v: %v", valueSpec.Names[i].Name, path)
}
}
}
}
// checkCopyLocksCompositeLit detects lock copy inside a composite literal
func checkCopyLocksCompositeLit(f *File, cl *ast.CompositeLit) {
for _, x := range cl.Elts {
if node, ok := x.(*ast.KeyValueExpr); ok {
x = node.Value
}
if path := lockPathRhs(f, x); path != nil {
f.Badf(x.Pos(), "literal copies lock value from %v: %v", f.gofmt(x), path)
}
}
}
// checkCopyLocksReturnStmt detects lock copy in return statement
func checkCopyLocksReturnStmt(f *File, rs *ast.ReturnStmt) {
for _, x := range rs.Results {
if path := lockPathRhs(f, x); path != nil {
f.Badf(x.Pos(), "return copies lock value: %v", path)
}
}
}
// checkCopyLocksCallExpr detects lock copy in the arguments to a function call
func checkCopyLocksCallExpr(f *File, ce *ast.CallExpr) {
if id, ok := ce.Fun.(*ast.Ident); ok && id.Name == "new" && f.pkg.types[id].IsBuiltin() {
// Skip 'new(Type)' for built-in 'new'
return
}
for _, x := range ce.Args {
if path := lockPathRhs(f, x); path != nil {
f.Badf(x.Pos(), "function call copies lock value: %v", path)
}
}
}
// checkCopyLocksFunc checks whether a function might
// inadvertently copy a lock, by checking whether
// its receiver, parameters, or return values
// are locks.
func checkCopyLocksFunc(f *File, name string, recv *ast.FieldList, typ *ast.FuncType) {
if recv != nil && len(recv.List) > 0 {
expr := recv.List[0].Type
if path := lockPath(f.pkg.typesPkg, f.pkg.types[expr].Type); path != nil {
f.Badf(expr.Pos(), "%s passes lock by value: %v", name, path)
}
}
if typ.Params != nil {
for _, field := range typ.Params.List {
expr := field.Type
if path := lockPath(f.pkg.typesPkg, f.pkg.types[expr].Type); path != nil {
f.Badf(expr.Pos(), "%s passes lock by value: %v", name, path)
}
}
}
// Don't check typ.Results. If T has a Lock field it's OK to write
// return T{}
// because that is returning the zero value. Leave result checking
// to the return statement.
}
// checkCopyLocksRange checks whether a range statement
// might inadvertently copy a lock by checking whether
// any of the range variables are locks.
func checkCopyLocksRange(f *File, r *ast.RangeStmt) {
checkCopyLocksRangeVar(f, r.Tok, r.Key)
checkCopyLocksRangeVar(f, r.Tok, r.Value)
}
func checkCopyLocksRangeVar(f *File, rtok token.Token, e ast.Expr) {
if e == nil {
return
}
id, isId := e.(*ast.Ident)
if isId && id.Name == "_" {
return
}
var typ types.Type
if rtok == token.DEFINE {
if !isId {
return
}
obj := f.pkg.defs[id]
if obj == nil {
return
}
typ = obj.Type()
} else {
typ = f.pkg.types[e].Type
}
if typ == nil {
return
}
if path := lockPath(f.pkg.typesPkg, typ); path != nil {
f.Badf(e.Pos(), "range var %s copies lock: %v", f.gofmt(e), path)
}
}
type typePath []types.Type
// String pretty-prints a typePath.
func (path typePath) String() string {
n := len(path)
var buf bytes.Buffer
for i := range path {
if i > 0 {
fmt.Fprint(&buf, " contains ")
}
// The human-readable path is in reverse order, outermost to innermost.
fmt.Fprint(&buf, path[n-i-1].String())
}
return buf.String()
}
func lockPathRhs(f *File, x ast.Expr) typePath {
if _, ok := x.(*ast.CompositeLit); ok {
return nil
}
if _, ok := x.(*ast.CallExpr); ok {
// A call may return a zero value.
return nil
}
if star, ok := x.(*ast.StarExpr); ok {
if _, ok := star.X.(*ast.CallExpr); ok {
// A call may return a pointer to a zero value.
return nil
}
}
return lockPath(f.pkg.typesPkg, f.pkg.types[x].Type)
}
// lockPath returns a typePath describing the location of a lock value
// contained in typ. If there is no contained lock, it returns nil.
func lockPath(tpkg *types.Package, typ types.Type) typePath {
if typ == nil {
return nil
}
// We're only interested in the case in which the underlying
// type is a struct. (Interfaces and pointers are safe to copy.)
styp, ok := typ.Underlying().(*types.Struct)
if !ok {
return nil
}
// We're looking for cases in which a reference to this type
// can be locked, but a value cannot. This differentiates
// embedded interfaces from embedded values.
if plock := types.NewMethodSet(types.NewPointer(typ)).Lookup(tpkg, "Lock"); plock != nil {
if lock := types.NewMethodSet(typ).Lookup(tpkg, "Lock"); lock == nil {
return []types.Type{typ}
}
}
nfields := styp.NumFields()
for i := 0; i < nfields; i++ {
ftyp := styp.Field(i).Type()
subpath := lockPath(tpkg, ftyp)
if subpath != nil {
return append(subpath, typ)
}
}
return nil
}

View file

@ -0,0 +1,298 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Check for syntactically unreachable code.
package main
import (
"go/ast"
"go/token"
)
func init() {
register("unreachable",
"check for unreachable code",
checkUnreachable,
funcDecl, funcLit)
}
type deadState struct {
f *File
hasBreak map[ast.Stmt]bool
hasGoto map[string]bool
labels map[string]ast.Stmt
breakTarget ast.Stmt
reachable bool
}
// checkUnreachable checks a function body for dead code.
//
// TODO(adonovan): use the new cfg package, which is more precise.
func checkUnreachable(f *File, node ast.Node) {
var body *ast.BlockStmt
switch n := node.(type) {
case *ast.FuncDecl:
body = n.Body
case *ast.FuncLit:
body = n.Body
}
if body == nil {
return
}
d := &deadState{
f: f,
hasBreak: make(map[ast.Stmt]bool),
hasGoto: make(map[string]bool),
labels: make(map[string]ast.Stmt),
}
d.findLabels(body)
d.reachable = true
d.findDead(body)
}
// findLabels gathers information about the labels defined and used by stmt
// and about which statements break, whether a label is involved or not.
func (d *deadState) findLabels(stmt ast.Stmt) {
switch x := stmt.(type) {
default:
d.f.Warnf(x.Pos(), "internal error in findLabels: unexpected statement %T", x)
case *ast.AssignStmt,
*ast.BadStmt,
*ast.DeclStmt,
*ast.DeferStmt,
*ast.EmptyStmt,
*ast.ExprStmt,
*ast.GoStmt,
*ast.IncDecStmt,
*ast.ReturnStmt,
*ast.SendStmt:
// no statements inside
case *ast.BlockStmt:
for _, stmt := range x.List {
d.findLabels(stmt)
}
case *ast.BranchStmt:
switch x.Tok {
case token.GOTO:
if x.Label != nil {
d.hasGoto[x.Label.Name] = true
}
case token.BREAK:
stmt := d.breakTarget
if x.Label != nil {
stmt = d.labels[x.Label.Name]
}
if stmt != nil {
d.hasBreak[stmt] = true
}
}
case *ast.IfStmt:
d.findLabels(x.Body)
if x.Else != nil {
d.findLabels(x.Else)
}
case *ast.LabeledStmt:
d.labels[x.Label.Name] = x.Stmt
d.findLabels(x.Stmt)
// These cases are all the same, but the x.Body only works
// when the specific type of x is known, so the cases cannot
// be merged.
case *ast.ForStmt:
outer := d.breakTarget
d.breakTarget = x
d.findLabels(x.Body)
d.breakTarget = outer
case *ast.RangeStmt:
outer := d.breakTarget
d.breakTarget = x
d.findLabels(x.Body)
d.breakTarget = outer
case *ast.SelectStmt:
outer := d.breakTarget
d.breakTarget = x
d.findLabels(x.Body)
d.breakTarget = outer
case *ast.SwitchStmt:
outer := d.breakTarget
d.breakTarget = x
d.findLabels(x.Body)
d.breakTarget = outer
case *ast.TypeSwitchStmt:
outer := d.breakTarget
d.breakTarget = x
d.findLabels(x.Body)
d.breakTarget = outer
case *ast.CommClause:
for _, stmt := range x.Body {
d.findLabels(stmt)
}
case *ast.CaseClause:
for _, stmt := range x.Body {
d.findLabels(stmt)
}
}
}
// findDead walks the statement looking for dead code.
// If d.reachable is false on entry, stmt itself is dead.
// When findDead returns, d.reachable tells whether the
// statement following stmt is reachable.
func (d *deadState) findDead(stmt ast.Stmt) {
// Is this a labeled goto target?
// If so, assume it is reachable due to the goto.
// This is slightly conservative, in that we don't
// check that the goto is reachable, so
// L: goto L
// will not provoke a warning.
// But it's good enough.
if x, isLabel := stmt.(*ast.LabeledStmt); isLabel && d.hasGoto[x.Label.Name] {
d.reachable = true
}
if !d.reachable {
switch stmt.(type) {
case *ast.EmptyStmt:
// do not warn about unreachable empty statements
default:
d.f.Bad(stmt.Pos(), "unreachable code")
d.reachable = true // silence error about next statement
}
}
switch x := stmt.(type) {
default:
d.f.Warnf(x.Pos(), "internal error in findDead: unexpected statement %T", x)
case *ast.AssignStmt,
*ast.BadStmt,
*ast.DeclStmt,
*ast.DeferStmt,
*ast.EmptyStmt,
*ast.GoStmt,
*ast.IncDecStmt,
*ast.SendStmt:
// no control flow
case *ast.BlockStmt:
for _, stmt := range x.List {
d.findDead(stmt)
}
case *ast.BranchStmt:
switch x.Tok {
case token.BREAK, token.GOTO, token.FALLTHROUGH:
d.reachable = false
case token.CONTINUE:
// NOTE: We accept "continue" statements as terminating.
// They are not necessary in the spec definition of terminating,
// because a continue statement cannot be the final statement
// before a return. But for the more general problem of syntactically
// identifying dead code, continue redirects control flow just
// like the other terminating statements.
d.reachable = false
}
case *ast.ExprStmt:
// Call to panic?
call, ok := x.X.(*ast.CallExpr)
if ok {
name, ok := call.Fun.(*ast.Ident)
if ok && name.Name == "panic" && name.Obj == nil {
d.reachable = false
}
}
case *ast.ForStmt:
d.findDead(x.Body)
d.reachable = x.Cond != nil || d.hasBreak[x]
case *ast.IfStmt:
d.findDead(x.Body)
if x.Else != nil {
r := d.reachable
d.reachable = true
d.findDead(x.Else)
d.reachable = d.reachable || r
} else {
// might not have executed if statement
d.reachable = true
}
case *ast.LabeledStmt:
d.findDead(x.Stmt)
case *ast.RangeStmt:
d.findDead(x.Body)
d.reachable = true
case *ast.ReturnStmt:
d.reachable = false
case *ast.SelectStmt:
// NOTE: Unlike switch and type switch below, we don't care
// whether a select has a default, because a select without a
// default blocks until one of the cases can run. That's different
// from a switch without a default, which behaves like it has
// a default with an empty body.
anyReachable := false
for _, comm := range x.Body.List {
d.reachable = true
for _, stmt := range comm.(*ast.CommClause).Body {
d.findDead(stmt)
}
anyReachable = anyReachable || d.reachable
}
d.reachable = anyReachable || d.hasBreak[x]
case *ast.SwitchStmt:
anyReachable := false
hasDefault := false
for _, cas := range x.Body.List {
cc := cas.(*ast.CaseClause)
if cc.List == nil {
hasDefault = true
}
d.reachable = true
for _, stmt := range cc.Body {
d.findDead(stmt)
}
anyReachable = anyReachable || d.reachable
}
d.reachable = anyReachable || d.hasBreak[x] || !hasDefault
case *ast.TypeSwitchStmt:
anyReachable := false
hasDefault := false
for _, cas := range x.Body.List {
cc := cas.(*ast.CaseClause)
if cc.List == nil {
hasDefault = true
}
d.reachable = true
for _, stmt := range cc.Body {
d.findDead(stmt)
}
anyReachable = anyReachable || d.reachable
}
d.reachable = anyReachable || d.hasBreak[x] || !hasDefault
}
}

View file

@ -0,0 +1,205 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Vet examines Go source code and reports suspicious constructs, such as Printf
calls whose arguments do not align with the format string. Vet uses heuristics
that do not guarantee all reports are genuine problems, but it can find errors
not caught by the compilers.
It can be invoked three ways:
By package, from the go tool:
go vet package/path/name
vets the package whose path is provided.
By files:
go tool vet source/directory/*.go
vets the files named, all of which must be in the same package.
By directory:
go tool vet source/directory
recursively descends the directory, vetting each package it finds.
Vet's exit code is 2 for erroneous invocation of the tool, 1 if a
problem was reported, and 0 otherwise. Note that the tool does not
check every possible problem and depends on unreliable heuristics
so it should be used as guidance only, not as a firm indicator of
program correctness.
By default the -all flag is set so all checks are performed.
If any flags are explicitly set to true, only those tests are run. Conversely, if
any flag is explicitly set to false, only those tests are disabled. Thus -printf=true
runs the printf check, -printf=false runs all checks except the printf check.
Available checks:
Assembly declarations
Flag: -asmdecl
Mismatches between assembly files and Go function declarations.
Useless assignments
Flag: -assign
Check for useless assignments.
Atomic mistakes
Flag: -atomic
Common mistaken usages of the sync/atomic package.
Boolean conditions
Flag: -bool
Mistakes involving boolean operators.
Build tags
Flag: -buildtags
Badly formed or misplaced +build tags.
Invalid uses of cgo
Flag: -cgocall
Detect some violations of the cgo pointer passing rules.
Unkeyed composite literals
Flag: -composites
Composite struct literals that do not use the field-keyed syntax.
Copying locks
Flag: -copylocks
Locks that are erroneously passed by value.
Tests, benchmarks and documentation examples
Flag: -tests
Mistakes involving tests including functions with incorrect names or signatures
and example tests that document identifiers not in the package.
Failure to call the cancelation function returned by context.WithCancel.
Flag: -lostcancel
The cancelation function returned by context.WithCancel, WithTimeout,
and WithDeadline must be called or the new context will remain live
until its parent context is cancelled.
(The background context is never cancelled.)
Methods
Flag: -methods
Non-standard signatures for methods with familiar names, including:
Format GobEncode GobDecode MarshalJSON MarshalXML
Peek ReadByte ReadFrom ReadRune Scan Seek
UnmarshalJSON UnreadByte UnreadRune WriteByte
WriteTo
Nil function comparison
Flag: -nilfunc
Comparisons between functions and nil.
Printf family
Flag: -printf
Suspicious calls to functions in the Printf family, including any functions
with these names, disregarding case:
Print Printf Println
Fprint Fprintf Fprintln
Sprint Sprintf Sprintln
Error Errorf
Fatal Fatalf
Log Logf
Panic Panicf Panicln
The -printfuncs flag can be used to redefine this list.
If the function name ends with an 'f', the function is assumed to take
a format descriptor string in the manner of fmt.Printf. If not, vet
complains about arguments that look like format descriptor strings.
It also checks for errors such as using a Writer as the first argument of
Printf.
Struct tags
Range loop variables
Flag: -rangeloops
Incorrect uses of range loop variables in closures.
Shadowed variables
Flag: -shadow=false (experimental; must be set explicitly)
Variables that may have been unintentionally shadowed.
Shifts
Flag: -shift
Shifts equal to or longer than the variable's length.
Flag: -structtags
Struct tags that do not follow the format understood by reflect.StructTag.Get.
Well-known encoding struct tags (json, xml) used with unexported fields.
Unreachable code
Flag: -unreachable
Unreachable code.
Misuse of unsafe Pointers
Flag: -unsafeptr
Likely incorrect uses of unsafe.Pointer to convert integers to pointers.
A conversion from uintptr to unsafe.Pointer is invalid if it implies that
there is a uintptr-typed word in memory that holds a pointer value,
because that word will be invisible to stack copying and to the garbage
collector.
Unused result of certain function calls
Flag: -unusedresult
Calls to well-known functions and methods that return a value that is
discarded. By default, this includes functions like fmt.Errorf and
fmt.Sprintf and methods like String and Error. The flags -unusedfuncs
and -unusedstringmethods control the set.
Other flags
These flags configure the behavior of vet:
-all (default true)
Enable all non-experimental checks.
-v
Verbose mode
-printfuncs
A comma-separated list of print-like function names
to supplement the standard list.
For more information, see the discussion of the -printf flag.
-shadowstrict
Whether to be strict about shadowing; can be noisy.
*/
package main

View file

@ -0,0 +1,512 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cfg
// This file implements the CFG construction pass.
import (
"fmt"
"go/ast"
"go/token"
)
type builder struct {
cfg *CFG
mayReturn func(*ast.CallExpr) bool
current *Block
lblocks map[*ast.Object]*lblock // labeled blocks
targets *targets // linked stack of branch targets
}
func (b *builder) stmt(_s ast.Stmt) {
// The label of the current statement. If non-nil, its _goto
// target is always set; its _break and _continue are set only
// within the body of switch/typeswitch/select/for/range.
// It is effectively an additional default-nil parameter of stmt().
var label *lblock
start:
switch s := _s.(type) {
case *ast.BadStmt,
*ast.SendStmt,
*ast.IncDecStmt,
*ast.GoStmt,
*ast.DeferStmt,
*ast.EmptyStmt,
*ast.AssignStmt:
// No effect on control flow.
b.add(s)
case *ast.ExprStmt:
b.add(s)
if call, ok := s.X.(*ast.CallExpr); ok && !b.mayReturn(call) {
// Calls to panic, os.Exit, etc, never return.
b.current = b.newUnreachableBlock("unreachable.call")
}
case *ast.DeclStmt:
// Treat each var ValueSpec as a separate statement.
d := s.Decl.(*ast.GenDecl)
if d.Tok == token.VAR {
for _, spec := range d.Specs {
if spec, ok := spec.(*ast.ValueSpec); ok {
b.add(spec)
}
}
}
case *ast.LabeledStmt:
label = b.labeledBlock(s.Label)
b.jump(label._goto)
b.current = label._goto
_s = s.Stmt
goto start // effectively: tailcall stmt(g, s.Stmt, label)
case *ast.ReturnStmt:
b.add(s)
b.current = b.newUnreachableBlock("unreachable.return")
case *ast.BranchStmt:
var block *Block
switch s.Tok {
case token.BREAK:
if s.Label != nil {
if lb := b.labeledBlock(s.Label); lb != nil {
block = lb._break
}
} else {
for t := b.targets; t != nil && block == nil; t = t.tail {
block = t._break
}
}
case token.CONTINUE:
if s.Label != nil {
if lb := b.labeledBlock(s.Label); lb != nil {
block = lb._continue
}
} else {
for t := b.targets; t != nil && block == nil; t = t.tail {
block = t._continue
}
}
case token.FALLTHROUGH:
for t := b.targets; t != nil; t = t.tail {
block = t._fallthrough
}
case token.GOTO:
if s.Label != nil {
block = b.labeledBlock(s.Label)._goto
}
}
if block == nil {
block = b.newBlock("undefined.branch")
}
b.jump(block)
b.current = b.newUnreachableBlock("unreachable.branch")
case *ast.BlockStmt:
b.stmtList(s.List)
case *ast.IfStmt:
if s.Init != nil {
b.stmt(s.Init)
}
then := b.newBlock("if.then")
done := b.newBlock("if.done")
_else := done
if s.Else != nil {
_else = b.newBlock("if.else")
}
b.add(s.Cond)
b.ifelse(then, _else)
b.current = then
b.stmt(s.Body)
b.jump(done)
if s.Else != nil {
b.current = _else
b.stmt(s.Else)
b.jump(done)
}
b.current = done
case *ast.SwitchStmt:
b.switchStmt(s, label)
case *ast.TypeSwitchStmt:
b.typeSwitchStmt(s, label)
case *ast.SelectStmt:
b.selectStmt(s, label)
case *ast.ForStmt:
b.forStmt(s, label)
case *ast.RangeStmt:
b.rangeStmt(s, label)
default:
panic(fmt.Sprintf("unexpected statement kind: %T", s))
}
}
func (b *builder) stmtList(list []ast.Stmt) {
for _, s := range list {
b.stmt(s)
}
}
func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) {
if s.Init != nil {
b.stmt(s.Init)
}
if s.Tag != nil {
b.add(s.Tag)
}
done := b.newBlock("switch.done")
if label != nil {
label._break = done
}
// We pull the default case (if present) down to the end.
// But each fallthrough label must point to the next
// body block in source order, so we preallocate a
// body block (fallthru) for the next case.
// Unfortunately this makes for a confusing block order.
var defaultBody *[]ast.Stmt
var defaultFallthrough *Block
var fallthru, defaultBlock *Block
ncases := len(s.Body.List)
for i, clause := range s.Body.List {
body := fallthru
if body == nil {
body = b.newBlock("switch.body") // first case only
}
// Preallocate body block for the next case.
fallthru = done
if i+1 < ncases {
fallthru = b.newBlock("switch.body")
}
cc := clause.(*ast.CaseClause)
if cc.List == nil {
// Default case.
defaultBody = &cc.Body
defaultFallthrough = fallthru
defaultBlock = body
continue
}
var nextCond *Block
for _, cond := range cc.List {
nextCond = b.newBlock("switch.next")
b.add(cond) // one half of the tag==cond condition
b.ifelse(body, nextCond)
b.current = nextCond
}
b.current = body
b.targets = &targets{
tail: b.targets,
_break: done,
_fallthrough: fallthru,
}
b.stmtList(cc.Body)
b.targets = b.targets.tail
b.jump(done)
b.current = nextCond
}
if defaultBlock != nil {
b.jump(defaultBlock)
b.current = defaultBlock
b.targets = &targets{
tail: b.targets,
_break: done,
_fallthrough: defaultFallthrough,
}
b.stmtList(*defaultBody)
b.targets = b.targets.tail
}
b.jump(done)
b.current = done
}
func (b *builder) typeSwitchStmt(s *ast.TypeSwitchStmt, label *lblock) {
if s.Init != nil {
b.stmt(s.Init)
}
if s.Assign != nil {
b.add(s.Assign)
}
done := b.newBlock("typeswitch.done")
if label != nil {
label._break = done
}
var default_ *ast.CaseClause
for _, clause := range s.Body.List {
cc := clause.(*ast.CaseClause)
if cc.List == nil {
default_ = cc
continue
}
body := b.newBlock("typeswitch.body")
var next *Block
for _, casetype := range cc.List {
next = b.newBlock("typeswitch.next")
// casetype is a type, so don't call b.add(casetype).
// This block logically contains a type assertion,
// x.(casetype), but it's unclear how to represent x.
_ = casetype
b.ifelse(body, next)
b.current = next
}
b.current = body
b.typeCaseBody(cc, done)
b.current = next
}
if default_ != nil {
b.typeCaseBody(default_, done)
} else {
b.jump(done)
}
b.current = done
}
func (b *builder) typeCaseBody(cc *ast.CaseClause, done *Block) {
b.targets = &targets{
tail: b.targets,
_break: done,
}
b.stmtList(cc.Body)
b.targets = b.targets.tail
b.jump(done)
}
func (b *builder) selectStmt(s *ast.SelectStmt, label *lblock) {
// First evaluate channel expressions.
// TODO(adonovan): fix: evaluate only channel exprs here.
for _, clause := range s.Body.List {
if comm := clause.(*ast.CommClause).Comm; comm != nil {
b.stmt(comm)
}
}
done := b.newBlock("select.done")
if label != nil {
label._break = done
}
var defaultBody *[]ast.Stmt
for _, cc := range s.Body.List {
clause := cc.(*ast.CommClause)
if clause.Comm == nil {
defaultBody = &clause.Body
continue
}
body := b.newBlock("select.body")
next := b.newBlock("select.next")
b.ifelse(body, next)
b.current = body
b.targets = &targets{
tail: b.targets,
_break: done,
}
switch comm := clause.Comm.(type) {
case *ast.ExprStmt: // <-ch
// nop
case *ast.AssignStmt: // x := <-states[state].Chan
b.add(comm.Lhs[0])
}
b.stmtList(clause.Body)
b.targets = b.targets.tail
b.jump(done)
b.current = next
}
if defaultBody != nil {
b.targets = &targets{
tail: b.targets,
_break: done,
}
b.stmtList(*defaultBody)
b.targets = b.targets.tail
b.jump(done)
}
b.current = done
}
func (b *builder) forStmt(s *ast.ForStmt, label *lblock) {
// ...init...
// jump loop
// loop:
// if cond goto body else done
// body:
// ...body...
// jump post
// post: (target of continue)
// ...post...
// jump loop
// done: (target of break)
if s.Init != nil {
b.stmt(s.Init)
}
body := b.newBlock("for.body")
done := b.newBlock("for.done") // target of 'break'
loop := body // target of back-edge
if s.Cond != nil {
loop = b.newBlock("for.loop")
}
cont := loop // target of 'continue'
if s.Post != nil {
cont = b.newBlock("for.post")
}
if label != nil {
label._break = done
label._continue = cont
}
b.jump(loop)
b.current = loop
if loop != body {
b.add(s.Cond)
b.ifelse(body, done)
b.current = body
}
b.targets = &targets{
tail: b.targets,
_break: done,
_continue: cont,
}
b.stmt(s.Body)
b.targets = b.targets.tail
b.jump(cont)
if s.Post != nil {
b.current = cont
b.stmt(s.Post)
b.jump(loop) // back-edge
}
b.current = done
}
func (b *builder) rangeStmt(s *ast.RangeStmt, label *lblock) {
b.add(s.X)
if s.Key != nil {
b.add(s.Key)
}
if s.Value != nil {
b.add(s.Value)
}
// ...
// loop: (target of continue)
// if ... goto body else done
// body:
// ...
// jump loop
// done: (target of break)
loop := b.newBlock("range.loop")
b.jump(loop)
b.current = loop
body := b.newBlock("range.body")
done := b.newBlock("range.done")
b.ifelse(body, done)
b.current = body
if label != nil {
label._break = done
label._continue = loop
}
b.targets = &targets{
tail: b.targets,
_break: done,
_continue: loop,
}
b.stmt(s.Body)
b.targets = b.targets.tail
b.jump(loop) // back-edge
b.current = done
}
// -------- helpers --------
// Destinations associated with unlabeled for/switch/select stmts.
// We push/pop one of these as we enter/leave each construct and for
// each BranchStmt we scan for the innermost target of the right type.
//
type targets struct {
tail *targets // rest of stack
_break *Block
_continue *Block
_fallthrough *Block
}
// Destinations associated with a labeled block.
// We populate these as labels are encountered in forward gotos or
// labeled statements.
//
type lblock struct {
_goto *Block
_break *Block
_continue *Block
}
// labeledBlock returns the branch target associated with the
// specified label, creating it if needed.
//
func (b *builder) labeledBlock(label *ast.Ident) *lblock {
lb := b.lblocks[label.Obj]
if lb == nil {
lb = &lblock{_goto: b.newBlock(label.Name)}
if b.lblocks == nil {
b.lblocks = make(map[*ast.Object]*lblock)
}
b.lblocks[label.Obj] = lb
}
return lb
}
// newBlock appends a new unconnected basic block to b.cfg's block
// slice and returns it.
// It does not automatically become the current block.
// comment is an optional string for more readable debugging output.
func (b *builder) newBlock(comment string) *Block {
g := b.cfg
block := &Block{
index: int32(len(g.Blocks)),
comment: comment,
}
block.Succs = block.succs2[:0]
g.Blocks = append(g.Blocks, block)
return block
}
func (b *builder) newUnreachableBlock(comment string) *Block {
block := b.newBlock(comment)
block.unreachable = true
return block
}
func (b *builder) add(n ast.Node) {
b.current.Nodes = append(b.current.Nodes, n)
}
// jump adds an edge from the current block to the target block,
// and sets b.current to nil.
func (b *builder) jump(target *Block) {
b.current.Succs = append(b.current.Succs, target)
b.current = nil
}
// ifelse emits edges from the current block to the t and f blocks,
// and sets b.current to nil.
func (b *builder) ifelse(t, f *Block) {
b.current.Succs = append(b.current.Succs, t, f)
b.current = nil
}

View file

@ -0,0 +1,142 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This package constructs a simple control-flow graph (CFG) of the
// statements and expressions within a single function.
//
// Use cfg.New to construct the CFG for a function body.
//
// The blocks of the CFG contain all the function's non-control
// statements. The CFG does not contain control statements such as If,
// Switch, Select, and Branch, but does contain their subexpressions.
// For example, this source code:
//
// if x := f(); x != nil {
// T()
// } else {
// F()
// }
//
// produces this CFG:
//
// 1: x := f()
// x != nil
// succs: 2, 3
// 2: T()
// succs: 4
// 3: F()
// succs: 4
// 4:
//
// The CFG does contain Return statements; even implicit returns are
// materialized (at the position of the function's closing brace).
//
// The CFG does not record conditions associated with conditional branch
// edges, nor the short-circuit semantics of the && and || operators,
// nor abnormal control flow caused by panic. If you need this
// information, use golang.org/x/tools/go/ssa instead.
//
package cfg
// Although the vet tool has type information, it is often extremely
// fragmentary, so for simplicity this package does not depend on
// go/types. Consequently control-flow conditions are ignored even
// when constant, and "mayReturn" information must be provided by the
// client.
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/token"
)
// A CFG represents the control-flow graph of a single function.
//
// The entry point is Blocks[0]; there may be multiple return blocks.
type CFG struct {
Blocks []*Block // block[0] is entry; order otherwise undefined
}
// A Block represents a basic block: a list of statements and
// expressions that are always evaluated sequentially.
//
// A block may have 0-2 successors: zero for a return block or a block
// that calls a function such as panic that never returns; one for a
// normal (jump) block; and two for a conditional (if) block.
type Block struct {
Nodes []ast.Node // statements, expressions, and ValueSpecs
Succs []*Block // successor nodes in the graph
comment string // for debugging
index int32 // index within CFG.Blocks
unreachable bool // is block of stmts following return/panic/for{}
succs2 [2]*Block // underlying array for Succs
}
// New returns a new control-flow graph for the specified function body,
// which must be non-nil.
//
// The CFG builder calls mayReturn to determine whether a given function
// call may return. For example, calls to panic, os.Exit, and log.Fatal
// do not return, so the builder can remove infeasible graph edges
// following such calls. The builder calls mayReturn only for a
// CallExpr beneath an ExprStmt.
func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG {
b := builder{
mayReturn: mayReturn,
cfg: new(CFG),
}
b.current = b.newBlock("entry")
b.stmt(body)
// Does control fall off the end of the function's body?
// Make implicit return explicit.
if b.current != nil && !b.current.unreachable {
b.add(&ast.ReturnStmt{
Return: body.End() - 1,
})
}
return b.cfg
}
func (b *Block) String() string {
return fmt.Sprintf("block %d (%s)", b.index, b.comment)
}
// Return returns the return statement at the end of this block if present, nil otherwise.
func (b *Block) Return() (ret *ast.ReturnStmt) {
if len(b.Nodes) > 0 {
ret, _ = b.Nodes[len(b.Nodes)-1].(*ast.ReturnStmt)
}
return
}
// Format formats the control-flow graph for ease of debugging.
func (g *CFG) Format(fset *token.FileSet) string {
var buf bytes.Buffer
for _, b := range g.Blocks {
fmt.Fprintf(&buf, ".%d: # %s\n", b.index, b.comment)
for _, n := range b.Nodes {
fmt.Fprintf(&buf, "\t%s\n", formatNode(fset, n))
}
if len(b.Succs) > 0 {
fmt.Fprintf(&buf, "\tsuccs:")
for _, succ := range b.Succs {
fmt.Fprintf(&buf, " %d", succ.index)
}
buf.WriteByte('\n')
}
buf.WriteByte('\n')
}
return buf.String()
}
func formatNode(fset *token.FileSet, n ast.Node) string {
var buf bytes.Buffer
format.Node(&buf, fset, n)
// Indent secondary lines by a tab.
return string(bytes.Replace(buf.Bytes(), []byte("\n"), []byte("\n\t"), -1))
}

View file

@ -0,0 +1,28 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package whitelist defines exceptions for the vet tool.
package whitelist
// UnkeyedLiteral is a white list of types in the standard packages
// that are used with unkeyed literals we deem to be acceptable.
var UnkeyedLiteral = map[string]bool{
// These image and image/color struct types are frozen. We will never add fields to them.
"image/color.Alpha16": true,
"image/color.Alpha": true,
"image/color.CMYK": true,
"image/color.Gray16": true,
"image/color.Gray": true,
"image/color.NRGBA64": true,
"image/color.NRGBA": true,
"image/color.NYCbCrA": true,
"image/color.RGBA64": true,
"image/color.RGBA": true,
"image/color.YCbCr": true,
"image.Point": true,
"image.Rectangle": true,
"image.Uniform": true,
"unicode.Range16": true,
}

View file

@ -0,0 +1,318 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"github.com/dnephin/govet/internal/cfg"
"fmt"
"go/ast"
"go/types"
"strconv"
)
func init() {
register("lostcancel",
"check for failure to call cancelation function returned by context.WithCancel",
checkLostCancel,
funcDecl, funcLit)
}
const debugLostCancel = false
var contextPackage = "context"
// checkLostCancel reports a failure to the call the cancel function
// returned by context.WithCancel, either because the variable was
// assigned to the blank identifier, or because there exists a
// control-flow path from the call to a return statement and that path
// does not "use" the cancel function. Any reference to the variable
// counts as a use, even within a nested function literal.
//
// checkLostCancel analyzes a single named or literal function.
func checkLostCancel(f *File, node ast.Node) {
// Fast path: bypass check if file doesn't use context.WithCancel.
if !hasImport(f.file, contextPackage) {
return
}
// Maps each cancel variable to its defining ValueSpec/AssignStmt.
cancelvars := make(map[*types.Var]ast.Node)
// Find the set of cancel vars to analyze.
stack := make([]ast.Node, 0, 32)
ast.Inspect(node, func(n ast.Node) bool {
switch n.(type) {
case *ast.FuncLit:
if len(stack) > 0 {
return false // don't stray into nested functions
}
case nil:
stack = stack[:len(stack)-1] // pop
return true
}
stack = append(stack, n) // push
// Look for [{AssignStmt,ValueSpec} CallExpr SelectorExpr]:
//
// ctx, cancel := context.WithCancel(...)
// ctx, cancel = context.WithCancel(...)
// var ctx, cancel = context.WithCancel(...)
//
if isContextWithCancel(f, n) && isCall(stack[len(stack)-2]) {
var id *ast.Ident // id of cancel var
stmt := stack[len(stack)-3]
switch stmt := stmt.(type) {
case *ast.ValueSpec:
if len(stmt.Names) > 1 {
id = stmt.Names[1]
}
case *ast.AssignStmt:
if len(stmt.Lhs) > 1 {
id, _ = stmt.Lhs[1].(*ast.Ident)
}
}
if id != nil {
if id.Name == "_" {
f.Badf(id.Pos(), "the cancel function returned by context.%s should be called, not discarded, to avoid a context leak",
n.(*ast.SelectorExpr).Sel.Name)
} else if v, ok := f.pkg.uses[id].(*types.Var); ok {
cancelvars[v] = stmt
} else if v, ok := f.pkg.defs[id].(*types.Var); ok {
cancelvars[v] = stmt
}
}
}
return true
})
if len(cancelvars) == 0 {
return // no need to build CFG
}
// Tell the CFG builder which functions never return.
info := &types.Info{Uses: f.pkg.uses, Selections: f.pkg.selectors}
mayReturn := func(call *ast.CallExpr) bool {
name := callName(info, call)
return !noReturnFuncs[name]
}
// Build the CFG.
var g *cfg.CFG
var sig *types.Signature
switch node := node.(type) {
case *ast.FuncDecl:
sig, _ = f.pkg.defs[node.Name].Type().(*types.Signature)
g = cfg.New(node.Body, mayReturn)
case *ast.FuncLit:
sig, _ = f.pkg.types[node.Type].Type.(*types.Signature)
g = cfg.New(node.Body, mayReturn)
}
// Print CFG.
if debugLostCancel {
fmt.Println(g.Format(f.fset))
}
// Examine the CFG for each variable in turn.
// (It would be more efficient to analyze all cancelvars in a
// single pass over the AST, but seldom is there more than one.)
for v, stmt := range cancelvars {
if ret := lostCancelPath(f, g, v, stmt, sig); ret != nil {
lineno := f.fset.Position(stmt.Pos()).Line
f.Badf(stmt.Pos(), "the %s function is not used on all paths (possible context leak)", v.Name())
f.Badf(ret.Pos(), "this return statement may be reached without using the %s var defined on line %d", v.Name(), lineno)
}
}
}
func isCall(n ast.Node) bool { _, ok := n.(*ast.CallExpr); return ok }
func hasImport(f *ast.File, path string) bool {
for _, imp := range f.Imports {
v, _ := strconv.Unquote(imp.Path.Value)
if v == path {
return true
}
}
return false
}
// isContextWithCancel reports whether n is one of the qualified identifiers
// context.With{Cancel,Timeout,Deadline}.
func isContextWithCancel(f *File, n ast.Node) bool {
if sel, ok := n.(*ast.SelectorExpr); ok {
switch sel.Sel.Name {
case "WithCancel", "WithTimeout", "WithDeadline":
if x, ok := sel.X.(*ast.Ident); ok {
if pkgname, ok := f.pkg.uses[x].(*types.PkgName); ok {
return pkgname.Imported().Path() == contextPackage
}
// Import failed, so we can't check package path.
// Just check the local package name (heuristic).
return x.Name == "context"
}
}
}
return false
}
// lostCancelPath finds a path through the CFG, from stmt (which defines
// the 'cancel' variable v) to a return statement, that doesn't "use" v.
// If it finds one, it returns the return statement (which may be synthetic).
// sig is the function's type, if known.
func lostCancelPath(f *File, g *cfg.CFG, v *types.Var, stmt ast.Node, sig *types.Signature) *ast.ReturnStmt {
vIsNamedResult := sig != nil && tupleContains(sig.Results(), v)
// uses reports whether stmts contain a "use" of variable v.
uses := func(f *File, v *types.Var, stmts []ast.Node) bool {
found := false
for _, stmt := range stmts {
ast.Inspect(stmt, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.Ident:
if f.pkg.uses[n] == v {
found = true
}
case *ast.ReturnStmt:
// A naked return statement counts as a use
// of the named result variables.
if n.Results == nil && vIsNamedResult {
found = true
}
}
return !found
})
}
return found
}
// blockUses computes "uses" for each block, caching the result.
memo := make(map[*cfg.Block]bool)
blockUses := func(f *File, v *types.Var, b *cfg.Block) bool {
res, ok := memo[b]
if !ok {
res = uses(f, v, b.Nodes)
memo[b] = res
}
return res
}
// Find the var's defining block in the CFG,
// plus the rest of the statements of that block.
var defblock *cfg.Block
var rest []ast.Node
outer:
for _, b := range g.Blocks {
for i, n := range b.Nodes {
if n == stmt {
defblock = b
rest = b.Nodes[i+1:]
break outer
}
}
}
if defblock == nil {
panic("internal error: can't find defining block for cancel var")
}
// Is v "used" in the remainder of its defining block?
if uses(f, v, rest) {
return nil
}
// Does the defining block return without using v?
if ret := defblock.Return(); ret != nil {
return ret
}
// Search the CFG depth-first for a path, from defblock to a
// return block, in which v is never "used".
seen := make(map[*cfg.Block]bool)
var search func(blocks []*cfg.Block) *ast.ReturnStmt
search = func(blocks []*cfg.Block) *ast.ReturnStmt {
for _, b := range blocks {
if !seen[b] {
seen[b] = true
// Prune the search if the block uses v.
if blockUses(f, v, b) {
continue
}
// Found path to return statement?
if ret := b.Return(); ret != nil {
if debugLostCancel {
fmt.Printf("found path to return in block %s\n", b)
}
return ret // found
}
// Recur
if ret := search(b.Succs); ret != nil {
if debugLostCancel {
fmt.Printf(" from block %s\n", b)
}
return ret
}
}
}
return nil
}
return search(defblock.Succs)
}
func tupleContains(tuple *types.Tuple, v *types.Var) bool {
for i := 0; i < tuple.Len(); i++ {
if tuple.At(i) == v {
return true
}
}
return false
}
var noReturnFuncs = map[string]bool{
"(*testing.common).FailNow": true,
"(*testing.common).Fatal": true,
"(*testing.common).Fatalf": true,
"(*testing.common).Skip": true,
"(*testing.common).SkipNow": true,
"(*testing.common).Skipf": true,
"log.Fatal": true,
"log.Fatalf": true,
"log.Fatalln": true,
"os.Exit": true,
"panic": true,
"runtime.Goexit": true,
}
// callName returns the canonical name of the builtin, method, or
// function called by call, if known.
func callName(info *types.Info, call *ast.CallExpr) string {
switch fun := call.Fun.(type) {
case *ast.Ident:
// builtin, e.g. "panic"
if obj, ok := info.Uses[fun].(*types.Builtin); ok {
return obj.Name()
}
case *ast.SelectorExpr:
if sel, ok := info.Selections[fun]; ok && sel.Kind() == types.MethodVal {
// method call, e.g. "(*testing.common).Fatal"
meth := sel.Obj()
return fmt.Sprintf("(%s).%s",
meth.Type().(*types.Signature).Recv().Type(),
meth.Name())
}
if obj, ok := info.Uses[fun.Sel]; ok {
// qualified identifier, e.g. "os.Exit"
return fmt.Sprintf("%s.%s",
obj.Pkg().Path(),
obj.Name())
}
}
// function with no name, or defined in missing imported package
return ""
}

View file

@ -0,0 +1,504 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Vet is a simple checker for static errors in Go source code.
// See doc.go for more information.
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/printer"
"go/token"
"go/types"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
)
var (
verbose = flag.Bool("v", false, "verbose")
tags = flag.String("tags", "", "comma-separated list of build tags to apply when parsing")
noRecurse = flag.Bool("no-recurse", false, "disable recursive directory walking")
tagList = []string{} // exploded version of tags flag; set in main
)
var exitCode = 0
// "-all" flag enables all non-experimental checks
var all = triStateFlag("all", unset, "enable all non-experimental checks")
// Flags to control which individual checks to perform.
var report = map[string]*triState{
// Only unusual checks are written here.
// Most checks that operate during the AST walk are added by register.
"asmdecl": triStateFlag("asmdecl", unset, "check assembly against Go declarations"),
"buildtags": triStateFlag("buildtags", unset, "check that +build tags are valid"),
}
// experimental records the flags enabling experimental features. These must be
// requested explicitly; they are not enabled by -all.
var experimental = map[string]bool{}
// setTrueCount record how many flags are explicitly set to true.
var setTrueCount int
// dirsRun and filesRun indicate whether the vet is applied to directory or
// file targets. The distinction affects which checks are run.
var dirsRun, filesRun bool
// includesNonTest indicates whether the vet is applied to non-test targets.
// Certain checks are relevant only if they touch both test and non-test files.
var includesNonTest bool
// A triState is a boolean that knows whether it has been set to either true or false.
// It is used to identify if a flag appears; the standard boolean flag cannot
// distinguish missing from unset. It also satisfies flag.Value.
type triState int
const (
unset triState = iota
setTrue
setFalse
)
func triStateFlag(name string, value triState, usage string) *triState {
flag.Var(&value, name, usage)
return &value
}
// triState implements flag.Value, flag.Getter, and flag.boolFlag.
// They work like boolean flags: we can say vet -printf as well as vet -printf=true
func (ts *triState) Get() interface{} {
return *ts == setTrue
}
func (ts triState) isTrue() bool {
return ts == setTrue
}
func (ts *triState) Set(value string) error {
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
if b {
*ts = setTrue
setTrueCount++
} else {
*ts = setFalse
}
return nil
}
func (ts *triState) String() string {
switch *ts {
case unset:
return "true" // An unset flag will be set by -all, so defaults to true.
case setTrue:
return "true"
case setFalse:
return "false"
}
panic("not reached")
}
func (ts triState) IsBoolFlag() bool {
return true
}
// vet tells whether to report errors for the named check, a flag name.
func vet(name string) bool {
return report[name].isTrue()
}
// setExit sets the value for os.Exit when it is called, later. It
// remembers the highest value.
func setExit(err int) {
if err > exitCode {
exitCode = err
}
}
var (
// Each of these vars has a corresponding case in (*File).Visit.
assignStmt *ast.AssignStmt
binaryExpr *ast.BinaryExpr
callExpr *ast.CallExpr
compositeLit *ast.CompositeLit
exprStmt *ast.ExprStmt
field *ast.Field
funcDecl *ast.FuncDecl
funcLit *ast.FuncLit
genDecl *ast.GenDecl
interfaceType *ast.InterfaceType
rangeStmt *ast.RangeStmt
returnStmt *ast.ReturnStmt
// checkers is a two-level map.
// The outer level is keyed by a nil pointer, one of the AST vars above.
// The inner level is keyed by checker name.
checkers = make(map[ast.Node]map[string]func(*File, ast.Node))
)
func register(name, usage string, fn func(*File, ast.Node), types ...ast.Node) {
report[name] = triStateFlag(name, unset, usage)
for _, typ := range types {
m := checkers[typ]
if m == nil {
m = make(map[string]func(*File, ast.Node))
checkers[typ] = m
}
m[name] = fn
}
}
// Usage is a replacement usage function for the flags package.
func Usage() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\tvet [flags] directory...\n")
fmt.Fprintf(os.Stderr, "\tvet [flags] files... # Must be a single package\n")
fmt.Fprintf(os.Stderr, "By default, -all is set and all non-experimental checks are run.\n")
fmt.Fprintf(os.Stderr, "For more information run\n")
fmt.Fprintf(os.Stderr, "\tgo doc cmd/vet\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
os.Exit(2)
}
// File is a wrapper for the state of a file used in the parser.
// The parse tree walkers are all methods of this type.
type File struct {
pkg *Package
fset *token.FileSet
name string
content []byte
file *ast.File
b bytes.Buffer // for use by methods
// Parsed package "foo" when checking package "foo_test"
basePkg *Package
// The objects that are receivers of a "String() string" method.
// This is used by the recursiveStringer method in print.go.
stringers map[*ast.Object]bool
// Registered checkers to run.
checkers map[ast.Node][]func(*File, ast.Node)
}
func main() {
flag.Usage = Usage
flag.Parse()
// If any flag is set, we run only those checks requested.
// If all flag is set true or if no flags are set true, set all the non-experimental ones
// not explicitly set (in effect, set the "-all" flag).
if setTrueCount == 0 || *all == setTrue {
for name, setting := range report {
if *setting == unset && !experimental[name] {
*setting = setTrue
}
}
}
tagList = strings.Split(*tags, ",")
initPrintFlags()
initUnusedFlags()
if flag.NArg() == 0 {
Usage()
}
for _, name := range flag.Args() {
// Is it a directory?
fi, err := os.Stat(name)
if err != nil {
warnf("error walking tree: %s", err)
continue
}
if fi.IsDir() {
dirsRun = true
} else {
filesRun = true
if !strings.HasSuffix(name, "_test.go") {
includesNonTest = true
}
}
}
if dirsRun && filesRun {
Usage()
}
if dirsRun {
for _, name := range flag.Args() {
if *noRecurse {
doPackageDir(name)
} else {
walkDir(name)
}
}
os.Exit(exitCode)
}
if doPackage(".", flag.Args(), nil) == nil {
warnf("no files checked")
}
os.Exit(exitCode)
}
// prefixDirectory places the directory name on the beginning of each name in the list.
func prefixDirectory(directory string, names []string) {
if directory != "." {
for i, name := range names {
names[i] = filepath.Join(directory, name)
}
}
}
// doPackageDir analyzes the single package found in the directory, if there is one,
// plus a test package, if there is one.
func doPackageDir(directory string) {
context := build.Default
if len(context.BuildTags) != 0 {
warnf("build tags %s previously set", context.BuildTags)
}
context.BuildTags = append(tagList, context.BuildTags...)
pkg, err := context.ImportDir(directory, 0)
if err != nil {
// If it's just that there are no go source files, that's fine.
if _, nogo := err.(*build.NoGoError); nogo {
return
}
// Non-fatal: we are doing a recursive walk and there may be other directories.
warnf("cannot process directory %s: %s", directory, err)
return
}
var names []string
names = append(names, pkg.GoFiles...)
names = append(names, pkg.CgoFiles...)
names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
names = append(names, pkg.SFiles...)
prefixDirectory(directory, names)
basePkg := doPackage(directory, names, nil)
// Is there also a "foo_test" package? If so, do that one as well.
if len(pkg.XTestGoFiles) > 0 {
names = pkg.XTestGoFiles
prefixDirectory(directory, names)
doPackage(directory, names, basePkg)
}
}
type Package struct {
path string
defs map[*ast.Ident]types.Object
uses map[*ast.Ident]types.Object
selectors map[*ast.SelectorExpr]*types.Selection
types map[ast.Expr]types.TypeAndValue
spans map[types.Object]Span
files []*File
typesPkg *types.Package
}
// doPackage analyzes the single package constructed from the named files.
// It returns the parsed Package or nil if none of the files have been checked.
func doPackage(directory string, names []string, basePkg *Package) *Package {
var files []*File
var astFiles []*ast.File
fs := token.NewFileSet()
for _, name := range names {
data, err := ioutil.ReadFile(name)
if err != nil {
// Warn but continue to next package.
warnf("%s: %s", name, err)
return nil
}
checkBuildTag(name, data)
var parsedFile *ast.File
if strings.HasSuffix(name, ".go") {
parsedFile, err = parser.ParseFile(fs, name, data, 0)
if err != nil {
warnf("%s: %s", name, err)
return nil
}
astFiles = append(astFiles, parsedFile)
}
files = append(files, &File{fset: fs, content: data, name: name, file: parsedFile})
}
if len(astFiles) == 0 {
return nil
}
pkg := new(Package)
pkg.path = astFiles[0].Name.Name
pkg.files = files
// Type check the package.
err := pkg.check(fs, astFiles)
if err != nil && *verbose {
warnf("%s", err)
}
// Check.
chk := make(map[ast.Node][]func(*File, ast.Node))
for typ, set := range checkers {
for name, fn := range set {
if vet(name) {
chk[typ] = append(chk[typ], fn)
}
}
}
for _, file := range files {
file.pkg = pkg
file.basePkg = basePkg
file.checkers = chk
if file.file != nil {
file.walkFile(file.name, file.file)
}
}
asmCheck(pkg)
return pkg
}
func visit(path string, f os.FileInfo, err error) error {
if err != nil {
warnf("walk error: %s", err)
return err
}
// One package per directory. Ignore the files themselves.
if !f.IsDir() {
return nil
}
doPackageDir(path)
return nil
}
func (pkg *Package) hasFileWithSuffix(suffix string) bool {
for _, f := range pkg.files {
if strings.HasSuffix(f.name, suffix) {
return true
}
}
return false
}
// walkDir recursively walks the tree looking for Go packages.
func walkDir(root string) {
filepath.Walk(root, visit)
}
// errorf formats the error to standard error, adding program
// identification and a newline, and exits.
func errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "vet: "+format+"\n", args...)
os.Exit(2)
}
// warnf formats the error to standard error, adding program
// identification and a newline, but does not exit.
func warnf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "vet: "+format+"\n", args...)
setExit(1)
}
// Println is fmt.Println guarded by -v.
func Println(args ...interface{}) {
if !*verbose {
return
}
fmt.Println(args...)
}
// Printf is fmt.Printf guarded by -v.
func Printf(format string, args ...interface{}) {
if !*verbose {
return
}
fmt.Printf(format+"\n", args...)
}
// Bad reports an error and sets the exit code..
func (f *File) Bad(pos token.Pos, args ...interface{}) {
f.Warn(pos, args...)
setExit(1)
}
// Badf reports a formatted error and sets the exit code.
func (f *File) Badf(pos token.Pos, format string, args ...interface{}) {
f.Warnf(pos, format, args...)
setExit(1)
}
// loc returns a formatted representation of the position.
func (f *File) loc(pos token.Pos) string {
if pos == token.NoPos {
return ""
}
// Do not print columns. Because the pos often points to the start of an
// expression instead of the inner part with the actual error, the
// precision can mislead.
posn := f.fset.Position(pos)
return fmt.Sprintf("%s:%d", posn.Filename, posn.Line)
}
// Warn reports an error but does not set the exit code.
func (f *File) Warn(pos token.Pos, args ...interface{}) {
fmt.Fprintf(os.Stderr, "%s: %s", f.loc(pos), fmt.Sprintln(args...))
}
// Warnf reports a formatted error but does not set the exit code.
func (f *File) Warnf(pos token.Pos, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "%s: %s\n", f.loc(pos), fmt.Sprintf(format, args...))
}
// walkFile walks the file's tree.
func (f *File) walkFile(name string, file *ast.File) {
Println("Checking file", name)
ast.Walk(f, file)
}
// Visit implements the ast.Visitor interface.
func (f *File) Visit(node ast.Node) ast.Visitor {
var key ast.Node
switch node.(type) {
case *ast.AssignStmt:
key = assignStmt
case *ast.BinaryExpr:
key = binaryExpr
case *ast.CallExpr:
key = callExpr
case *ast.CompositeLit:
key = compositeLit
case *ast.ExprStmt:
key = exprStmt
case *ast.Field:
key = field
case *ast.FuncDecl:
key = funcDecl
case *ast.FuncLit:
key = funcLit
case *ast.GenDecl:
key = genDecl
case *ast.InterfaceType:
key = interfaceType
case *ast.RangeStmt:
key = rangeStmt
case *ast.ReturnStmt:
key = returnStmt
}
for _, fn := range f.checkers[key] {
fn(f, node)
}
return f
}
// gofmt returns a string representation of the expression.
func (f *File) gofmt(x ast.Expr) string {
f.b.Reset()
printer.Fprint(&f.b, f.fset, x)
return f.b.String()
}

Some files were not shown because too many files have changed in this diff Show more