Merge branch 'main' into webfinger_rework

This commit is contained in:
tsmethurst 2022-06-11 10:44:00 +02:00
commit 54d9778c4c
215 changed files with 11375 additions and 3732 deletions

View file

@ -1,8 +1,9 @@
.github .github
cmd .vscode
archive
dist
docs docs
example example
internal
scripts scripts
test test
testrig testrig

View file

@ -38,6 +38,7 @@ steps:
- apk update --no-cache && apk add git - apk update --no-cache && apk add git
- CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./... - CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./...
- CGO_ENABLED=0 ./test/cliparsing.sh - CGO_ENABLED=0 ./test/cliparsing.sh
- CGO_ENABLED=0 ./test/envparsing.sh
when: when:
event: event:
include: include:
@ -145,6 +146,6 @@ steps:
--- ---
kind: signature kind: signature
hmac: f3cf4e422d9ce7dc0a881da429db628232e2f9e91383ee5a33cf4f13542c0a23 hmac: adfcc11559717e4e371e714f3ac19ab528208f678961436f316f491bf82de8ad
... ...

66
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View file

@ -0,0 +1,66 @@
name: Bug Report
description: Create a report to help us improve
title: "[bug] Issue Title"
labels: [bug]
assignees:
-
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please be cautious with the sensitive information/logs while filing the issue.
- type: textarea
id: desc
attributes:
label: Describe the bug with a clear and concise description of what the bug is.
validations:
required: true
- type: input
id: GoToSocial-Version
attributes:
label: What's your GoToSocial Version?
description: Enter the Version of your GoToSocial Installation
placeholder: e.g. v0.3.4
validations:
required: true
- type: input
id: GoToSocial-Arch
attributes:
label: GoToSocial Arch
description: What architecture do you use and did you do a Binary or Docker installation?
placeholder: e.g. arm64 Docker or arm64 Binary
validations:
required: false
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Enter exactly what happened.
validations:
required: false
- type: textarea
id: what-expected
attributes:
label: What you expected to happen?
description: Enter what you expected to happen.
validations:
required: false
- type: textarea
id: how-to-reproduce
attributes:
label: How to reproduce it?
description: As minimally and precisely as possible.
validations:
required: false
- type: textarea
id: anything-else
attributes:
label: Anything else we need to know?
validations:
required: false

8
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View file

@ -0,0 +1,8 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View file

@ -0,0 +1,44 @@
name: Feature request
description: Suggest an idea for this project
title: "[feature] Issue Title"
labels: [enhancement]
assignees:
-
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
- type: textarea
id: desc
attributes:
label: Is your feature request related to a problem ?
description: Give a clear and concise description of what the problem is.
placeholder: ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: prop-solution
attributes:
label: Describe the solution you'd like.
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered.
description: A clear and concise description of any alternative solutions or features you've considered. If nothing, please enter `NONE`
validations:
required: true
- type: textarea
id: additional-ctxt
attributes:
label: Additional context.
description: Add any other context or screenshots about the feature request here.
validations:
required: false

8
.gitignore vendored
View file

@ -20,4 +20,10 @@ dist/
web/assets/swagger.yaml web/assets/swagger.yaml
# exludes docker-volume from exemple/docker-compose # exludes docker-volume from exemple/docker-compose
example/docker-compose/docker-volume example/docker-compose/docker-volume
# excludes debug build
cmd/gotosocial/__debug_bin
# ignore f0x' nix-shell
shell.nix

View file

@ -3,16 +3,12 @@ project_name: gotosocial
before: before:
# https://goreleaser.com/customization/hooks/ # https://goreleaser.com/customization/hooks/
hooks: hooks:
# tidy up and lint
- go mod tidy
- go fmt ./...
# generate the swagger.yaml file using go-swagger and bundle it into the assets directory # generate the swagger.yaml file using go-swagger and bundle it into the assets directory
- swagger generate spec -o docs/api/swagger.yaml --scan-models - swagger generate spec -o web/assets/swagger.yaml --scan-models
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" docs/api/swagger.yaml - sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
- cp docs/api/swagger.yaml web/assets/swagger.yaml # bundle web assets
# install and bundle the web assets and styling - yarn install --cwd web/source
- yarn install --cwd web/gotosocial-styling - scripts/bundle.sh
- node web/gotosocial-styling/index.js --build-dir="web/assets"
builds: builds:
# https://goreleaser.com/customization/build/ # https://goreleaser.com/customization/build/
- -
@ -67,6 +63,10 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.version={{.Version}}"
extra_files: extra_files:
- web - web
- go.mod
- go.sum
- cmd
- internal
- -
use: buildx use: buildx
goos: linux goos: linux
@ -82,6 +82,10 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.version={{.Version}}"
extra_files: extra_files:
- web - web
- go.mod
- go.sum
- cmd
- internal
- -
use: buildx use: buildx
goos: linux goos: linux
@ -98,6 +102,10 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.version={{.Version}}"
extra_files: extra_files:
- web - web
- go.mod
- go.sum
- cmd
- internal
- -
use: buildx use: buildx
goos: linux goos: linux
@ -114,6 +122,10 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.version={{.Version}}"
extra_files: extra_files:
- web - web
- go.mod
- go.sum
- cmd
- internal
docker_manifests: docker_manifests:
- name_template: superseriousbusiness/{{ .ProjectName }}:{{ .Version }} - name_template: superseriousbusiness/{{ .ProjectName }}:{{ .Version }}
image_templates: image_templates:
@ -136,7 +148,8 @@ archives:
- README.md - README.md
- CHANGELOG* - CHANGELOG*
# web assets # web assets
- web - web/assets
- web/template
# example config files # example config files
- example/config.yaml - example/config.yaml
- example/gotosocial.service - example/gotosocial.service

16
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/gotosocial",
"args": [
"testrig", "start"
],
"cwd": "${workspaceFolder}"
}
]
}

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"go.lintTool":"golangci-lint",
"go.lintFlags": [
"--fast"
]
}

View file

@ -28,7 +28,8 @@ Check the [issues](https://github.com/superseriousbusiness/gotosocial/issues) to
- [Updating Swagger docs](#updating-swagger-docs) - [Updating Swagger docs](#updating-swagger-docs)
- [CI/CD configuration](#cicd-configuration) - [CI/CD configuration](#cicd-configuration)
- [Release Checklist](#release-checklist) - [Release Checklist](#release-checklist)
- [Building releases and Docker containers](#building-releases-and-docker-containers) - [What if something goes wrong?](#what-if-something-goes-wrong)
- [Building Docker containers](#building-docker-containers)
- [With GoReleaser](#with-goreleaser) - [With GoReleaser](#with-goreleaser)
- [Manually](#manually) - [Manually](#manually)
- [Financial Compensation](#financial-compensation) - [Financial Compensation](#financial-compensation)
@ -70,19 +71,21 @@ To work with the stylesheet for templates, you need [Node.js](https://nodejs.org
To install Yarn dependencies: To install Yarn dependencies:
```bash ```bash
yarn install --cwd web/gotosocial-styling yarn install --cwd web/source
``` ```
To recompile bundles: To recompile bundles:
```bash ```bash
node web/gotosocial-styling/index.js --build-dir="web/assets" BUDO_BUILD=1 node web/source
``` ```
You can do automatic live-reloads of bundles with: Or you can run livereloading in development. It will start a webserver (ip/port printed in console, default localhost:8081), while also keeping the bundles
up-to-date on disk. You can access the user/admin panels at localhost:8080/user, localhost:8080/admin, or run in tandem with the GoToSocial testrig, which will also
serve the updated bundles from disk.
``` bash ``` bash
NODE_ENV=development node web/gotosocial-styling/index.js --build-dir="web/assets" NODE_ENV=development node web/source
``` ```
### Golang forking quirks ### Golang forking quirks
@ -185,7 +188,7 @@ Finally, to run tests against both database types one after the other, use:
### CLI Tests ### CLI Tests
In [./test/cliparsing.sh](./test/cliparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected. In [./test/cliparsing.sh](./test/cliparsing.sh) and [./test/envparsing.sh](./test/envparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected.
Although these tests *are* part of the CI/CD testing process, you probably won't need to worry too much about running them yourself. That is, unless you're messing about with code inside the `main` package in `cmd/gotosocial`, or inside the `config` package in `internal/config`. Although these tests *are* part of the CI/CD testing process, you probably won't need to worry too much about running them yourself. That is, unless you're messing about with code inside the `main` package in `cmd/gotosocial`, or inside the `config` package in `internal/config`.
@ -299,7 +302,9 @@ That is: Delete the tag.
Either way, once we've fixed the issue, we just start from the top of this list again. Version numbers are cheap. It's cheap to burn them. Either way, once we've fixed the issue, we just start from the top of this list again. Version numbers are cheap. It's cheap to burn them.
## Building releases and Docker containers ## Building Docker containers
For both of the below methods, you need to have [Docker buildx](https://docs.docker.com/buildx/working-with-buildx/) installed.
### With GoReleaser ### With GoReleaser
@ -311,22 +316,30 @@ Normally, these processes are handled by Drone (see CI/CD above). However, you c
To do this, first [install GoReleaser](https://goreleaser.com/install/). To do this, first [install GoReleaser](https://goreleaser.com/install/).
Then, to create snapshot builds, do: Then install GoSwagger as described in [the Swagger section](#updating-swagger-docs).
Then install Node and Yarn as described in [Stylesheet / Web dev](#stylesheet--web-dev).
Finally, to create a snapshot build, do:
```bash ```bash
goreleaser release --rm-dist --snapshot goreleaser --rm-dist --snapshot
``` ```
If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and a snapshot Docker image should be built (check your terminal output for version). If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and snapshot Docker images should be built (check your terminal output for version).
### Manually ### Manually
If you prefer a simple approach with fewer dependencies, you can also just build a Docker container manually in the following way: If you prefer a simple approach to building a Docker container, with fewer dependencies, you can also just build in the following way:
```bash ```bash
./scripts/build.sh && docker build -t superseriousbusiness/gotosocial:latest . ./scripts/build.sh && docker buildx build -t superseriousbusiness/gotosocial:latest .
``` ```
The above command first builds the `gotosocial` binary, then invokes Docker buildx to build the container image.
You don't need to install go-swagger, Node, or Yarn to build Docker images this way.
## Financial Compensation ## Financial Compensation
Right now there's no structure in place for financial compensation for pull requests and code. This is simply because there's no money being made on the project apart from the very small weekly Liberapay donations. Right now there's no structure in place for financial compensation for pull requests and code. This is simply because there's no money being made on the project apart from the very small weekly Liberapay donations.

View file

@ -1,26 +1,31 @@
# syntax=docker/dockerfile:1.3 # syntax=docker/dockerfile:1.3
# stage 1: generate up-to-date swagger.yaml to put in the final container
FROM --platform=${BUILDPLATFORM} quay.io/goswagger/swagger:v0.29.0 AS swagger
# bundle the admin webapp COPY go.mod /go/src/github.com/superseriousbusiness/gotosocial/go.mod
FROM --platform=${BUILDPLATFORM} node:17.6.0-alpine3.15 AS admin_builder COPY go.sum /go/src/github.com/superseriousbusiness/gotosocial/go.sum
RUN apk update && apk upgrade --no-cache COPY cmd /go/src/github.com/superseriousbusiness/gotosocial/cmd
RUN apk add git COPY internal /go/src/github.com/superseriousbusiness/gotosocial/internal
WORKDIR /go/src/github.com/superseriousbusiness/gotosocial
RUN swagger generate spec -o /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml --scan-models
RUN git clone https://github.com/superseriousbusiness/gotosocial-admin # stage 2: generate the web/assets/dist bundles
WORKDIR /gotosocial-admin FROM --platform=${BUILDPLATFORM} node:16.15.1-alpine3.15 AS bundler
RUN npm install COPY web web
RUN node index.js RUN yarn install --cwd web/source && \
BUDO_BUILD=1 node web/source && \
rm -r web/source
FROM --platform=${TARGETPLATFORM} alpine:3.15.0 AS executor # stage 3: build the executor container
FROM --platform=${TARGETPLATFORM} alpine:3.15.4 as executor
# copy over the binary from the first stage # copy the dist binary created by goreleaser or build.sh
COPY --chown=1000:1000 gotosocial /gotosocial/gotosocial COPY --chown=1000:1000 gotosocial /gotosocial/gotosocial
# copy over the web directory with templates etc # copy over the web directories with templates, assets etc
COPY --chown=1000:1000 web /gotosocial/web COPY --chown=1000:1000 --from=bundler web /gotosocial/web
COPY --chown=1000:1000 --from=swagger /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml web/assets/swagger.yaml
# copy over the admin directory
COPY --chown=1000:1000 --from=admin_builder /gotosocial-admin/public /gotosocial/web/assets/admin
WORKDIR "/gotosocial" WORKDIR "/gotosocial"
ENTRYPOINT [ "/gotosocial/gotosocial", "server", "start" ] ENTRYPOINT [ "/gotosocial/gotosocial", "server", "start" ]

View file

@ -165,7 +165,7 @@
} }
// build client api modules // build client api modules
authModule := auth.New(dbService, oauthServer, idp) authModule := auth.New(dbService, idp, processor)
accountModule := account.New(processor) accountModule := account.New(processor)
instanceModule := instance.New(processor) instanceModule := instance.New(processor)
appsModule := app.New(processor) appsModule := app.New(processor)

View file

@ -108,7 +108,7 @@
} }
// build client api modules // build client api modules
authModule := auth.New(dbService, oauthServer, idp) authModule := auth.New(dbService, idp, processor)
accountModule := account.New(processor) accountModule := account.New(processor)
instanceModule := instance.New(processor) instanceModule := instance.New(processor)
appsModule := app.New(processor) appsModule := app.New(processor)

View file

@ -1,6 +1,6 @@
# Admin Control Panel # Admin Control Panel
[gotosocial-admin](https://github.com/superseriousbusiness/gotosocial-admin) is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin). The GoToSocial admin panel is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin).
## Using the panel ## Using the panel
To use the Admin API your account has to be promoted as such: To use the Admin API your account has to be promoted as such:
@ -16,11 +16,11 @@ any other client.
You can change the instance's settings like the title and descriptions, and add/remove/change domain blocks including a bulk import/export. You can change the instance's settings like the title and descriptions, and add/remove/change domain blocks including a bulk import/export.
## Installing the panel ## Building the panel
Build requirements: some version of [Node.js](https://nodejs.org) and yarn. Build requirements: some version of [Node.js](https://nodejs.org) and yarn.
``` ```
git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin yarn install --cwd web/source
yarn install BUDO_BUILD=1 node web/source
node index.js
``` ```
This will compile a static bundle in `public/`, which can be copied to any webhost, or put into your GoToSocial installation in the `web/admin` directory.
See also: [Contributing.md Stylesheet / Web dev](https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#stylesheet--web-dev)

View file

@ -692,7 +692,7 @@ definitions:
text_url: text_url:
description: |- description: |-
A shorter URL for the attachment. A shorter URL for the attachment.
Not currently used. In our case, we just give the URL again since we don't create smaller URLs.
type: string type: string
x-go-name: TextURL x-go-name: TextURL
type: type:
@ -1894,8 +1894,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500": "500":
description: internal error description: internal server error
security: security:
- OAuth2 Application: - OAuth2 Application:
- write:accounts - write:accounts
@ -1924,6 +1926,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -1952,6 +1958,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:blocks - write:blocks
@ -1999,6 +2009,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:follows - write:follows
@ -2029,6 +2043,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -2059,6 +2077,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -2134,6 +2156,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -2162,6 +2188,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:blocks - write:blocks
@ -2190,6 +2220,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:follows - write:follows
@ -2215,6 +2249,12 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:accounts - write:accounts
@ -2247,6 +2287,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -2313,6 +2357,12 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:accounts - write:accounts
@ -2335,6 +2385,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -2372,6 +2426,12 @@ paths:
description: unauthorized description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2406,10 +2466,18 @@ paths:
$ref: '#/definitions/emoji' $ref: '#/definitions/emoji'
"400": "400":
description: bad request description: bad request
"401":
description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404":
description: not found
"406":
description: not acceptable
"409": "409":
description: conflict -- domain/shortcode combo for emoji already exists description: conflict -- domain/shortcode combo for emoji already exists
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2439,10 +2507,16 @@ paths:
type: array type: array
"400": "400":
description: bad request description: bad request
"401":
description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2511,8 +2585,16 @@ paths:
$ref: '#/definitions/domainBlock' $ref: '#/definitions/domainBlock'
"400": "400":
description: bad request description: bad request
"401":
description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2537,10 +2619,16 @@ paths:
$ref: '#/definitions/domainBlock' $ref: '#/definitions/domainBlock'
"400": "400":
description: bad request description: bad request
"401":
description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2564,10 +2652,16 @@ paths:
$ref: '#/definitions/domainBlock' $ref: '#/definitions/domainBlock'
"400": "400":
description: bad request description: bad request
"401":
description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2599,8 +2693,16 @@ paths:
asynchronously after the request completes. asynchronously after the request completes.
"400": "400":
description: bad request description: bad request
"401":
description: unauthorized
"403": "403":
description: forbidden description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2660,10 +2762,14 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"422": "403":
description: unprocessable description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500": "500":
description: internal error description: internal server error
summary: Register a new application on this instance. summary: Register a new application on this instance.
tags: tags:
- apps - apps
@ -2714,6 +2820,10 @@ paths:
description: unauthorized description: unauthorized
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:blocks - read:blocks
@ -2753,10 +2863,12 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:follows - read:follows
@ -2785,10 +2897,10 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500": "500":
description: internal server error description: internal server error
security: security:
@ -2817,10 +2929,10 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500": "500":
description: internal server error description: internal server error
security: security:
@ -2843,6 +2955,8 @@ paths:
description: Instance information. description: Instance information.
schema: schema:
$ref: '#/definitions/instance' $ref: '#/definitions/instance'
"406":
description: not acceptable
"500": "500":
description: internal error description: internal error
summary: View instance information. summary: View instance information.
@ -2909,6 +3023,14 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- admin - admin
@ -2952,10 +3074,10 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"422": "422":
description: unprocessable description: unprocessable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:media - write:media
@ -2982,10 +3104,12 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403": "404":
description: forbidden description: not found
"422": "406":
description: unprocessable description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:media - read:media
@ -3036,10 +3160,12 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403": "404":
description: forbidden description: not found
"422": "406":
description: unprocessable description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:media - write:media
@ -3141,6 +3267,12 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:search - read:search
@ -3226,10 +3358,14 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500": "500":
description: internal error description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:statuses - write:statuses
@ -3263,6 +3399,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:statuses - write:statuses
@ -3288,10 +3428,14 @@ paths:
description: bad request description: bad request
"401": "401":
description: unauthorized description: unauthorized
"403":
description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500": "500":
description: internal error description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:statuses - read:statuses
@ -3324,6 +3468,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:statuses - read:statuses
@ -3354,6 +3502,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:statuses - write:statuses
@ -3386,6 +3538,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- read:accounts - read:accounts
@ -3419,6 +3575,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:statuses - write:statuses
@ -3481,6 +3641,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:statuses - write:statuses
@ -3511,6 +3675,10 @@ paths:
description: forbidden description: forbidden
"404": "404":
description: not found description: not found
"406":
description: not acceptable
"500":
description: internal server error
security: security:
- OAuth2 Bearer: - OAuth2 Bearer:
- write:statuses - write:statuses
@ -3778,6 +3946,8 @@ paths:
description: unauthorized description: unauthorized
"403": "403":
description: forbidden description: forbidden
"406":
description: not acceptable
"500": "500":
description: internal error description: internal error
security: security:

View file

@ -0,0 +1,38 @@
# Advanced
Advanced settings options are provided for the sake of allowing admins to tune their instance to their liking.
These are set to sensible defaults, so most server admins won't need to touch them or think about them.
**Changing these settings if you don't know what you're doing may break your instance**.
## Settings
```yaml
#############################
##### ADVANCED SETTINGS #####
#############################
# Advanced settings pertaining to http timeouts, security, cookies, and more.
#
# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING!
#
# Most users will not need to (and should not) touch these settings, since
# they are set to sensible defaults, and may break if they are changed.
#
# Nevertheless, they are provided for the sake of allowing server admins to
# tweak their instance for performance or security reasons.
# String. Value of the SameSite attribute of cookies set by GoToSocial.
# Defaults to 'lax' to ensure that the OIDC flow does not break, which is
# fine in most cases. If you want to harden your instance against CSRF attacks
# and don't mind if some login-related things might break, you can set this
# to 'strict' instead.
#
# For an overview of what this does, see:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
#
# Options: ["lax", "strict"]
# Default: "lax"
advanced-cookies-samesite: "lax"
```

View file

@ -428,3 +428,30 @@ syslog-protocol: "udp"
# String. Address:port to send syslog logs to. Leave empty to connect to local syslog. # String. Address:port to send syslog logs to. Leave empty to connect to local syslog.
# Default: "localhost:514" # Default: "localhost:514"
syslog-address: "localhost:514" syslog-address: "localhost:514"
#############################
##### ADVANCED SETTINGS #####
#############################
# Advanced settings pertaining to http timeouts, security, cookies, and more.
#
# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING!
#
# Most users will not need to (and should not) touch these settings, since
# they are set to sensible defaults, and may break if they are changed.
#
# Nevertheless, they are provided for the sake of allowing server admins to
# tweak their instance for performance or security reasons.
# String. Value of the SameSite attribute of cookies set by GoToSocial.
# Defaults to 'lax' to ensure that the OIDC flow does not break, which is
# fine in most cases. If you want to harden your instance against CSRF attacks
# and don't mind if some login-related things might break, you can set this
# to 'strict' instead.
#
# For an overview of what this does, see:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
#
# Options: ["lax", "strict"]
# Default: "lax"
advanced-cookies-samesite: "lax"

2
go.sum
View file

@ -663,8 +663,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8= golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

View file

@ -23,12 +23,11 @@
"net" "net"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate" "github.com/superseriousbusiness/gotosocial/internal/validate"
) )
@ -61,58 +60,51 @@
// description: "An OAuth2 access token for the newly-created account." // description: "An OAuth2 access token for the newly-created account."
// schema: // schema:
// "$ref": "#/definitions/oauthToken" // "$ref": "#/definitions/oauthToken"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500': // '500':
// description: internal error // description: internal server error
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "accountCreatePOSTHandler")
authed, err := oauth.Authed(c, true, true, false, false) authed, err := oauth.Authed(c, true, true, false, false)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
l.Trace("parsing request form")
form := &model.AccountCreateRequest{} form := &model.AccountCreateRequest{}
if err := c.ShouldBind(form); err != nil || form == nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("could not parse form from request: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
return return
} }
l.Tracef("validating form %+v", form)
if err := validateCreateAccount(form); err != nil { if err := validateCreateAccount(form); err != nil {
l.Debugf("error validating form: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
clientIP := c.ClientIP() clientIP := c.ClientIP()
l.Tracef("attempting to parse client ip address %s", clientIP)
signUpIP := net.ParseIP(clientIP) signUpIP := net.ParseIP(clientIP)
if signUpIP == nil { if signUpIP == nil {
l.Debugf("error validating sign up ip address %s", clientIP) err := errors.New("ip address could not be parsed from request")
c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
form.IP = signUpIP form.IP = signUpIP
ti, err := m.processor.AccountCreate(c.Request.Context(), authed, form) ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form)
if err != nil { if errWithCode != nil {
l.Errorf("internal server error while creating new account: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
@ -122,6 +114,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
// validateCreateAccount checks through all the necessary prerequisites for creating a new account, // validateCreateAccount checks through all the necessary prerequisites for creating a new account,
// according to the provided account create request. If the account isn't eligible, an error will be returned. // according to the provided account create request. If the account isn't eligible, an error will be returned.
func validateCreateAccount(form *model.AccountCreateRequest) error { func validateCreateAccount(form *model.AccountCreateRequest) error {
if form == nil {
return errors.New("form was nil")
}
if !config.GetAccountsRegistrationOpen() { if !config.GetAccountsRegistrationOpen() {
return errors.New("registration is not open for this server") return errors.New("registration is not open for this server")
} }

View file

@ -19,12 +19,13 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -57,32 +58,35 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "AccountDeletePOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
l.Tracef("retrieved account %+v", authed.Account.ID)
form := &model.AccountDeleteRequest{} form := &model.AccountDeleteRequest{}
if err := c.ShouldBind(&form); err != nil { if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
if form.Password == "" { if form.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"}) err = errors.New("no password provided in account delete request")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
form.DeleteOriginID = authed.Account.ID form.DeleteOriginID = authed.Account.ID
if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil { if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil {
l.Debugf("could not delete account: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -19,11 +19,12 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -53,34 +54,38 @@
// '200': // '200':
// schema: // schema:
// "$ref": "#/definitions/account" // "$ref": "#/definitions/account"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountGETHandler(c *gin.Context) { func (m *Module) AccountGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID)
if err != nil { if errWithCode != nil {
logrus.Debug(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -19,15 +19,15 @@
package account package account
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -98,68 +98,67 @@
// description: "The newly updated account." // description: "The newly updated account."
// schema: // schema:
// "$ref": "#/definitions/account" // "$ref": "#/definitions/account"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
l := logrus.WithField("func", "accountUpdateCredentialsPATCHHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
l.Tracef("retrieved account %+v", authed.Account.ID)
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
form, err := parseUpdateAccountForm(c) form, err := parseUpdateAccountForm(c)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
// if everything on the form is nil, then nothing has been set and we shouldn't continue acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form)
if form.Discoverable == nil && if errWithCode != nil {
form.Bot == nil && api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
form.DisplayName == nil &&
form.Note == nil &&
form.Avatar == nil &&
form.Header == nil &&
form.Locked == nil &&
form.Source.Privacy == nil &&
form.Source.Sensitive == nil &&
form.Source.Language == nil &&
form.FieldsAttributes == nil {
l.Debugf("could not parse form from request")
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
return return
} }
acctSensitive, err := m.processor.AccountUpdate(c.Request.Context(), authed, form)
if err != nil {
l.Debugf("could not update account: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
l.Tracef("conversion successful, returning OK and apisensitive account %+v", acctSensitive)
c.JSON(http.StatusOK, acctSensitive) c.JSON(http.StatusOK, acctSensitive)
} }
func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) { func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) {
// parse main fields from request
form := &model.UpdateCredentialsRequest{ form := &model.UpdateCredentialsRequest{
Source: &model.UpdateSource{}, Source: &model.UpdateSource{},
} }
if err := c.ShouldBind(&form); err != nil || form == nil {
if err := c.ShouldBind(&form); err != nil {
return nil, fmt.Errorf("could not parse form from request: %s", err) return nil, fmt.Errorf("could not parse form from request: %s", err)
} }
if form == nil ||
(form.Discoverable == nil &&
form.Bot == nil &&
form.DisplayName == nil &&
form.Note == nil &&
form.Avatar == nil &&
form.Header == nil &&
form.Locked == nil &&
form.Source.Privacy == nil &&
form.Source.Sensitive == nil &&
form.Source.Language == nil &&
form.FieldsAttributes == nil) {
return nil, errors.New("empty form submitted")
}
// parse source field-by-field // parse source field-by-field
sourceMap := c.PostFormMap("source") sourceMap := c.PostFormMap("source")

View file

@ -26,7 +26,6 @@
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/account" "github.com/superseriousbusiness/gotosocial/internal/api/client/account"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@ -65,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
// check the response // check the response
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
// unmarshal the returned account // unmarshal the returned account
apimodelAccount := &apimodel.Account{} apimodelAccount := &apimodel.Account{}
@ -104,7 +103,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl
// check the response // check the response
b1, err := ioutil.ReadAll(result1.Body) b1, err := ioutil.ReadAll(result1.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
// unmarshal the returned account // unmarshal the returned account
apimodelAccount1 := &apimodel.Account{} apimodelAccount1 := &apimodel.Account{}
@ -185,7 +184,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet
// check the response // check the response
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
// unmarshal the returned account // unmarshal the returned account
apimodelAccount := &apimodel.Account{} apimodelAccount := &apimodel.Account{}
@ -227,7 +226,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo
// check the response // check the response
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
// unmarshal the returned account // unmarshal the returned account
apimodelAccount := &apimodel.Account{} apimodelAccount := &apimodel.Account{}
@ -271,7 +270,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit
// check the response // check the response
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
// unmarshal the returned account // unmarshal the returned account
apimodelAccount := &apimodel.Account{} apimodelAccount := &apimodel.Account{}
@ -313,8 +312,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp
// check the response // check the response
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
suite.Equal(`{"error":"empty form submitted"}`, string(b)) suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
} }
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() { func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() {
@ -348,7 +347,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd
// check the response // check the response
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) suite.NoError(err)
// unmarshal the returned account // unmarshal the returned account
apimodelAccount := &apimodel.Account{} apimodelAccount := &apimodel.Account{}

View file

@ -21,10 +21,9 @@
import ( import (
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -47,30 +46,31 @@
// '200': // '200':
// schema: // schema:
// "$ref": "#/definitions/account" // "$ref": "#/definitions/account"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountVerifyGETHandler(c *gin.Context) { func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
l := logrus.WithField("func", "accountVerifyGETHandler") authed, err := oauth.Authed(c, true, true, true, true)
authed, err := oauth.Authed(c, true, false, false, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
if err != nil { if errWithCode != nil {
l.Debugf("error getting account from processor: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return return
} }

View file

@ -19,10 +19,12 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -54,33 +56,38 @@
// description: Your relationship to this account. // description: Your relationship to this account.
// schema: // schema:
// "$ref": "#/definitions/accountRelationship" // "$ref": "#/definitions/accountRelationship"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID) relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -19,11 +19,13 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -75,39 +77,45 @@
// description: Your relationship to this account. // description: Your relationship to this account.
// schema: // schema:
// "$ref": "#/definitions/accountRelationship" // "$ref": "#/definitions/accountRelationship"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
form := &model.AccountFollowRequest{} form := &model.AccountFollowRequest{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
form.ID = targetAcctID form.ID = targetAcctID
relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form) relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -19,10 +19,12 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -56,33 +58,38 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/account" // "$ref": "#/definitions/account"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountFollowersGETHandler(c *gin.Context) { func (m *Module) AccountFollowersGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID) followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -19,10 +19,12 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -56,33 +58,38 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/account" // "$ref": "#/definitions/account"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountFollowingGETHandler(c *gin.Context) { func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID) following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -1,13 +1,13 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -43,24 +43,25 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/accountRelationship" // "$ref": "#/definitions/accountRelationship"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
l := logrus.WithField("func", "AccountRelationshipsGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -69,8 +70,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
// check fallback -- let's be generous and see if maybe it's just set as 'id'? // check fallback -- let's be generous and see if maybe it's just set as 'id'?
id := c.Query("id") id := c.Query("id")
if id == "" { if id == "" {
l.Debug("no account id specified in query") err = errors.New("no account id(s) specified in query")
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAccountIDs = append(targetAccountIDs, id) targetAccountIDs = append(targetAccountIDs, id)
@ -80,8 +81,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
for _, targetAccountID := range targetAccountIDs { for _, targetAccountID := range targetAccountIDs {
r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID) r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID)
if err != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
relationships = append(relationships, *r) relationships = append(relationships, *r)

View file

@ -19,13 +19,14 @@
package account package account
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -110,31 +111,32 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/status" // "$ref": "#/definitions/status"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountStatusesGETHandler(c *gin.Context) { func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
l := logrus.WithField("func", "AccountStatusesGETHandler")
authed, err := oauth.Authed(c, false, false, false, false) authed, err := oauth.Authed(c, false, false, false, false)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
l.Debug("no account id specified in query") err := errors.New("no account id specified")
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -143,8 +145,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -155,8 +157,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if excludeRepliesString != "" { if excludeRepliesString != "" {
i, err := strconv.ParseBool(excludeRepliesString) i, err := strconv.ParseBool(excludeRepliesString)
if err != nil { if err != nil {
l.Debugf("error parsing exclude replies string: %s", err) err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
excludeReplies = i excludeReplies = i
@ -167,8 +169,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if excludeReblogsString != "" { if excludeReblogsString != "" {
i, err := strconv.ParseBool(excludeReblogsString) i, err := strconv.ParseBool(excludeReblogsString)
if err != nil { if err != nil {
l.Debugf("error parsing exclude reblogs string: %s", err) err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
excludeReblogs = i excludeReblogs = i
@ -191,8 +193,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if pinnedString != "" { if pinnedString != "" {
i, err := strconv.ParseBool(pinnedString) i, err := strconv.ParseBool(pinnedString)
if err != nil { if err != nil {
l.Debugf("error parsing pinned string: %s", err) err := fmt.Errorf("error parsing %s: %s", PinnedKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
pinnedOnly = i pinnedOnly = i
@ -203,8 +205,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if mediaOnlyString != "" { if mediaOnlyString != "" {
i, err := strconv.ParseBool(mediaOnlyString) i, err := strconv.ParseBool(mediaOnlyString)
if err != nil { if err != nil {
l.Debugf("error parsing media only string: %s", err) err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse media only query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
mediaOnly = i mediaOnly = i
@ -215,19 +217,21 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if publicOnlyString != "" { if publicOnlyString != "" {
i, err := strconv.ParseBool(publicOnlyString) i, err := strconv.ParseBool(publicOnlyString)
if err != nil { if err != nil {
l.Debugf("error parsing public only string: %s", err) err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
publicOnly = i publicOnly = i
} }
statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error from processor account statuses get: %s", errWithCode) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
c.JSON(http.StatusOK, statuses) if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
c.JSON(http.StatusOK, resp.Items)
} }

View file

@ -37,7 +37,47 @@ type AccountStatusesTestSuite struct {
AccountStandardTestSuite AccountStandardTestSuite
} }
func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() { func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
// set up the request
// we're getting statuses of admin
targetAccount := suite.testAccounts["admin_account"]
recorder := httptest.NewRecorder()
ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=false&only_public=true", targetAccount.ID), "")
ctx.Params = gin.Params{
gin.Param{
Key: account.IDKey,
Value: targetAccount.ID,
},
}
// call the handler
suite.accountModule.AccountStatusesGETHandler(ctx)
// 1. we should have OK because our request was valid
suite.Equal(http.StatusOK, recorder.Code)
// 2. we should have no error message in the result body
result := recorder.Result()
defer result.Body.Close()
// check the response
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
// unmarshal the returned statuses
apimodelStatuses := []*apimodel.Status{}
err = json.Unmarshal(b, &apimodelStatuses)
suite.NoError(err)
suite.NotEmpty(apimodelStatuses)
for _, s := range apimodelStatuses {
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
}
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
}
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
// set up the request // set up the request
// we're getting statuses of admin // we're getting statuses of admin
targetAccount := suite.testAccounts["admin_account"] targetAccount := suite.testAccounts["admin_account"]
@ -74,6 +114,8 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
suite.NotEmpty(s.MediaAttachments) suite.NotEmpty(s.MediaAttachments)
suite.Equal(apimodel.VisibilityPublic, s.Visibility) suite.Equal(apimodel.VisibilityPublic, s.Visibility)
} }
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="prev"`, result.Header.Get("link"))
} }
func TestAccountStatusesTestSuite(t *testing.T) { func TestAccountStatusesTestSuite(t *testing.T) {

View file

@ -19,10 +19,12 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -54,33 +56,38 @@
// description: Your relationship to this account. // description: Your relationship to this account.
// schema: // schema:
// "$ref": "#/definitions/accountRelationship" // "$ref": "#/definitions/accountRelationship"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID) relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -19,12 +19,12 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -56,37 +56,38 @@
// description: Your relationship to this account. // description: Your relationship to this account.
// schema: // schema:
// "$ref": "#/definitions/accountRelationship" // "$ref": "#/definitions/accountRelationship"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "AccountUnfollowPOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debug(err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
l.Debug(err) err := errors.New("no account id specified")
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID) relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil { if errWithCode != nil {
l.Debug(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -19,12 +19,14 @@
package admin package admin
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -72,53 +74,47 @@
// description: unauthorized // description: unauthorized
// '403': // '403':
// description: forbidden // description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) AccountActionPOSTHandler(c *gin.Context) { func (m *Module) AccountActionPOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "AccountActionPOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed...
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
// with an admin account
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
// extract the form from the request context
l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.AdminAccountActionRequest{} form := &model.AdminAccountActionRequest{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("error parsing form %+v: %s", c.Request.Form, err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
return return
} }
if form.Type == "" { if form.Type == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no type specified"}) err := errors.New("no type specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetAcctID := c.Param(IDKey) targetAcctID := c.Param(IDKey)
if targetAcctID == "" { if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
form.TargetAccountID = targetAcctID form.TargetAccountID = targetAcctID
if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil { if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil {
l.Debugf("error performing account action: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -1,3 +1,21 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package admin package admin
import ( import (
@ -7,9 +25,9 @@
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -86,33 +104,33 @@
// Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead. // Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead.
// schema: // schema:
// "$ref": "#/definitions/domainBlock" // "$ref": "#/definitions/domainBlock"
// '403':
// description: forbidden
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "DomainBlocksPOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -121,49 +139,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
if importString != "" { if importString != "" {
i, err := strconv.ParseBool(importString) i, err := strconv.ParseBool(importString)
if err != nil { if err != nil {
l.Debugf("error parsing import string: %s", err) err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
imp = i imp = i
} }
// extract the media create form from the request context
l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.DomainBlockCreateRequest{} form := &model.DomainBlockCreateRequest{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("error parsing form %+v: %s", c.Request.Form, err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
return return
} }
// Give the fields on the request form a first pass to make sure the request is superficially valid.
l.Tracef("validating form %+v", form)
if err := validateCreateDomainBlock(form, imp); err != nil { if err := validateCreateDomainBlock(form, imp); err != nil {
l.Debugf("error validating form: %s", err) err := fmt.Errorf("error validating form: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
if imp { if imp {
// we're importing multiple blocks // we're importing multiple blocks
domainBlocks, err := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
if err != nil { if errWithCode != nil {
l.Debugf("error importing domain blocks: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
c.JSON(http.StatusOK, domainBlocks) c.JSON(http.StatusOK, domainBlocks)
} else { return
// we're just creating one block
domainBlock, err := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
if err != nil {
l.Debugf("error creating domain block: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, domainBlock)
} }
// we're just creating one block
domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
if errWithCode != nil {
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
c.JSON(http.StatusOK, domainBlock)
} }
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error { func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error {

View file

@ -1,11 +1,31 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package admin package admin
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -36,48 +56,46 @@
// description: The domain block that was just deleted. // description: The domain block that was just deleted.
// schema: // schema:
// "$ref": "#/definitions/domainBlock" // "$ref": "#/definitions/domainBlock"
// '403':
// description: forbidden
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "DomainBlockDELETEHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
domainBlockID := c.Param(IDKey) domainBlockID := c.Param(IDKey)
if domainBlockID == "" { if domainBlockID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) err := errors.New("no domain block id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID) domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error deleting domain block: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -1,12 +1,32 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package admin package admin
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -37,41 +57,40 @@
// description: The requested domain block. // description: The requested domain block.
// schema: // schema:
// "$ref": "#/definitions/domainBlock" // "$ref": "#/definitions/domainBlock"
// '403':
// description: forbidden
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainBlockGETHandler(c *gin.Context) { func (m *Module) DomainBlockGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "DomainBlockGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
domainBlockID := c.Param(IDKey) domainBlockID := c.Param(IDKey)
if domainBlockID == "" { if domainBlockID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) err := errors.New("no domain block id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -80,17 +99,16 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) {
if exportString != "" { if exportString != "" {
i, err := strconv.ParseBool(exportString) i, err := strconv.ParseBool(exportString)
if err != nil { if err != nil {
l.Debugf("error parsing export string: %s", err) err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
export = i export = i
} }
domainBlock, err := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
if err != nil { if errWithCode != nil {
l.Debugf("error getting domain block: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }

View file

@ -1,12 +1,31 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package admin package admin
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -43,35 +62,33 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/domainBlock" // "$ref": "#/definitions/domainBlock"
// '403':
// description: forbidden
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainBlocksGETHandler(c *gin.Context) { func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "DomainBlocksGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -80,17 +97,16 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
if exportString != "" { if exportString != "" {
i, err := strconv.ParseBool(exportString) i, err := strconv.ParseBool(exportString)
if err != nil { if err != nil {
l.Debugf("error parsing export string: %s", err) err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
export = i export = i
} }
domainBlocks, err := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
if err != nil { if errWithCode != nil {
l.Debugf("error getting domain blocks: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }

View file

@ -24,9 +24,9 @@
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate" "github.com/superseriousbusiness/gotosocial/internal/validate"
) )
@ -69,59 +69,52 @@
// description: The newly-created emoji. // description: The newly-created emoji.
// schema: // schema:
// "$ref": "#/definitions/emoji" // "$ref": "#/definitions/emoji"
// '403':
// description: forbidden
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '409': // '409':
// description: conflict -- domain/shortcode combo for emoji already exists // description: conflict -- domain/shortcode combo for emoji already exists
// '500':
// description: internal server error
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ authed, err := oauth.Authed(c, true, true, true, true)
"func": "emojiCreatePOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything*
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
// extract the media create form from the request context
l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.EmojiCreateRequest{} form := &model.EmojiCreateRequest{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("error parsing form %+v: %s", c.Request.Form, err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
return return
} }
// Give the fields on the request form a first pass to make sure the request is superficially valid.
l.Tracef("validating form %+v", form)
if err := validateCreateEmoji(form); err != nil { if err := validateCreateEmoji(form); err != nil {
l.Debugf("error validating form: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form) apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error creating emoji: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
@ -129,7 +122,6 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
} }
func validateCreateEmoji(form *model.EmojiCreateRequest) error { func validateCreateEmoji(form *model.EmojiCreateRequest) error {
// check there actually is an image attached and it's not size 0
if form.Image == nil || form.Image.Size == 0 { if form.Image == nil || form.Image.Size == 0 {
return errors.New("no emoji given") return errors.New("no emoji given")
} }

View file

@ -1,3 +1,21 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package admin_test package admin_test
import ( import (
@ -120,7 +138,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
suite.NoError(err) suite.NoError(err)
suite.NotEmpty(b) suite.NotEmpty(b)
suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b)) suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
} }
func TestEmojiCreateTestSuite(t *testing.T) { func TestEmojiCreateTestSuite(t *testing.T) {

View file

@ -23,9 +23,10 @@
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -54,39 +55,34 @@
// '200': // '200':
// description: |- // description: |-
// Echos the number of days requested. The cleanup is performed asynchronously after the request completes. // Echos the number of days requested. The cleanup is performed asynchronously after the request completes.
// '403':
// description: forbidden
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "MediaCleanupPOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
// make sure we're authed...
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
// with an admin account
if !authed.User.Admin { if !authed.User.Admin {
l.Debugf("user %s not an admin", authed.User.ID) err := fmt.Errorf("user %s not an admin", authed.User.ID)
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
// extract the form from the request context
l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.MediaCleanupRequest{} form := &model.MediaCleanupRequest{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("error parsing form %+v: %s", c.Request.Form, err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
return return
} }
@ -101,8 +97,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
} }
if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil {
l.Debugf("error starting prune of remote media: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -22,18 +22,16 @@
"fmt" "fmt"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// these consts are used to ensure users can't spam huge entries into our database
const ( const (
// permitted length for most fields formFieldLen = 64
formFieldLen = 64
// redirect can be a bit bigger because we probably need to encode data in the redirect uri
formRedirectLen = 512 formRedirectLen = 512
) )
@ -64,56 +62,63 @@
// description: "The newly-created application." // description: "The newly-created application."
// schema: // schema:
// "$ref": "#/definitions/application" // "$ref": "#/definitions/application"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '422': // '401':
// description: unprocessable // description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500': // '500':
// description: internal error // description: internal server error
func (m *Module) AppsPOSTHandler(c *gin.Context) { func (m *Module) AppsPOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "AppsPOSTHandler")
l.Trace("entering AppsPOSTHandler")
authed, err := oauth.Authed(c, false, false, false, false) authed, err := oauth.Authed(c, false, false, false, false)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
form := &model.ApplicationCreateRequest{} form := &model.ApplicationCreateRequest{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
// check lengths of fields before proceeding so the user can't spam huge entries into the database
if len(form.ClientName) > formFieldLen { if len(form.ClientName) > formFieldLen {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)}) err := fmt.Errorf("client_name must be less than %d bytes", formFieldLen)
return api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
}
if len(form.Website) > formFieldLen {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)})
return
}
if len(form.RedirectURIs) > formRedirectLen {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)})
return
}
if len(form.Scopes) > formFieldLen {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)})
return return
} }
apiApp, err := m.processor.AppCreate(c.Request.Context(), authed, form) if len(form.RedirectURIs) > formRedirectLen {
if err != nil { err := fmt.Errorf("redirect_uris must be less than %d bytes", formRedirectLen)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if len(form.Scopes) > formFieldLen {
err := fmt.Errorf("scopes must be less than %d bytes", formFieldLen)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if len(form.Website) > formFieldLen {
err := fmt.Errorf("website must be less than %d bytes", formFieldLen)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
apiApp, errWithCode := m.processor.AppCreate(c.Request.Context(), authed, form)
if errWithCode != nil {
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -23,8 +23,8 @@
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/oidc"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
) )
@ -66,17 +66,17 @@
// Module implements the ClientAPIModule interface for // Module implements the ClientAPIModule interface for
type Module struct { type Module struct {
db db.DB db db.DB
server oauth.Server idp oidc.IDP
idp oidc.IDP processor processing.Processor
} }
// New returns a new auth module // New returns a new auth module
func New(db db.DB, server oauth.Server, idp oidc.IDP) api.ClientModule { func New(db db.DB, idp oidc.IDP, processor processing.Processor) api.ClientModule {
return &Module{ return &Module{
db: db, db: db,
server: server, idp: idp,
idp: idp, processor: processor,
} }
} }

View file

@ -19,29 +19,42 @@
package auth_test package auth_test
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"net/http/httptest" "net/http/httptest"
"codeberg.org/gruf/go-store/kv"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore" "github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" "github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/oidc"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
type AuthStandardTestSuite struct { type AuthStandardTestSuite struct {
suite.Suite suite.Suite
db db.DB db db.DB
idp oidc.IDP storage *kv.KVStore
oauthServer oauth.Server mediaManager media.Manager
federator federation.Federator
processor processing.Processor
emailSender email.Sender
idp oidc.IDP
oauthServer oauth.Server
// standard suite models // standard suite models
testTokens map[string]*gtsmodel.Token testTokens map[string]*gtsmodel.Token
@ -69,39 +82,53 @@ func (suite *AuthStandardTestSuite) SetupSuite() {
func (suite *AuthStandardTestSuite) SetupTest() { func (suite *AuthStandardTestSuite) SetupTest() {
testrig.InitTestConfig() testrig.InitTestConfig()
suite.db = testrig.NewTestDB()
testrig.InitTestLog() testrig.InitTestLog()
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
suite.db = testrig.NewTestDB()
suite.storage = testrig.NewTestStorage()
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.oauthServer = testrig.NewTestOauthServer(suite.db)
var err error var err error
suite.idp, err = oidc.NewIDP(context.Background()) suite.idp, err = oidc.NewIDP(context.Background())
if err != nil { if err != nil {
panic(err) panic(err)
} }
suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp).(*auth.Module) suite.authModule = auth.New(suite.db, suite.idp, suite.processor).(*auth.Module)
testrig.StandardDBSetup(suite.db, nil) testrig.StandardDBSetup(suite.db, suite.testAccounts)
} }
func (suite *AuthStandardTestSuite) TearDownTest() { func (suite *AuthStandardTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db) testrig.StandardDBTeardown(suite.db)
} }
func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string) (*gin.Context, *httptest.ResponseRecorder) { func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string, requestBody []byte, bodyContentType string) (*gin.Context, *httptest.ResponseRecorder) {
// create the recorder and gin test context // create the recorder and gin test context
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
ctx, engine := gin.CreateTestContext(recorder) ctx, engine := gin.CreateTestContext(recorder)
// load templates into the engine // load templates into the engine
testrig.ConfigureTemplatesWithGin(engine) testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template")
// create the request // create the request
protocol := config.GetProtocol() protocol := config.GetProtocol()
host := config.GetHost() host := config.GetHost()
baseURI := fmt.Sprintf("%s://%s", protocol, host) baseURI := fmt.Sprintf("%s://%s", protocol, host)
requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath) requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath)
ctx.Request = httptest.NewRequest(requestMethod, requestURI, nil) // the endpoint we're hitting
ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting
ctx.Request.Header.Set("accept", "text/html") ctx.Request.Header.Set("accept", "text/html")
if bodyContentType != "" {
ctx.Request.Header.Set("Content-Type", bodyContentType)
}
// trigger the session middleware on the context // trigger the session middleware on the context
store := memstore.NewStore(make([]byte, 32), make([]byte, 32)) store := memstore.NewStore(make([]byte, 32), make([]byte, 32))
store.Options(router.SessionOptions()) store.Options(router.SessionOptions())

View file

@ -23,9 +23,6 @@
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/sirupsen/logrus"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -33,18 +30,22 @@
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
// helpfulAdvice is a handy hint to users;
// particularly important during the login flow
var helpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
// The idea here is to present an oauth authorize page to the user, with a button // The idea here is to present an oauth authorize page to the user, with a button
// that they have to click to accept. // that they have to click to accept.
func (m *Module) AuthorizeGETHandler(c *gin.Context) { func (m *Module) AuthorizeGETHandler(c *gin.Context) {
l := logrus.WithField("func", "AuthorizeGETHandler")
s := sessions.Default(c) s := sessions.Default(c)
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
c.HTML(http.StatusNotAcceptable, "error.tmpl", gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -52,56 +53,75 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. // If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
userID, ok := s.Get(sessionUserID).(string) userID, ok := s.Get(sessionUserID).(string)
if !ok || userID == "" { if !ok || userID == "" {
l.Trace("userid was empty, parsing form then redirecting to sign in page")
form := &model.OAuthAuthorize{} form := &model.OAuthAuthorize{}
if err := c.Bind(form); err != nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("invalid auth form: %s", err)
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return return
} }
l.Debugf("parsed auth form: %+v", form)
if err := extractAuthForm(s, form); err != nil { if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil {
l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err))
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
c.Redirect(http.StatusSeeOther, AuthSignInPath) c.Redirect(http.StatusSeeOther, AuthSignInPath)
return return
} }
// We can use the client_id on the session to retrieve info about the app associated with the client_id // use session information to validate app, user, and account for this request
clientID, ok := s.Get(sessionClientID).(string) clientID, ok := s.Get(sessionClientID).(string)
if !ok || clientID == "" { if !ok || clientID == "" {
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no client_id found in session"})
return
}
app := &gtsmodel.Application{}
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ err := fmt.Errorf("key %s was not found in session", sessionClientID)
"error": fmt.Sprintf("no application found for client id %s", clientID), api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
}) return
}
app := &gtsmodel.Application{}
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
m.clearSession(s)
safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
var errWithCode gtserror.WithCode
if err == db.ErrNoEntries {
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
} else {
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
}
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
// redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
// or thier account has been disabled.
user := &gtsmodel.User{} user := &gtsmodel.User{}
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
var errWithCode gtserror.WithCode
if err == db.ErrNoEntries {
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
} else {
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
}
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
if err != nil { if err != nil {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
var errWithCode gtserror.WithCode
if err == db.ErrNoEntries {
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
} else {
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
}
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
return return
} }
@ -109,25 +129,27 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
redirect, ok := s.Get(sessionRedirectURI).(string) redirect, ok := s.Get(sessionRedirectURI).(string)
if !ok || redirect == "" { if !ok || redirect == "" {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no redirect_uri found in session"}) err := fmt.Errorf("key %s was not found in session", sessionRedirectURI)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return return
} }
scope, ok := s.Get(sessionScope).(string) scope, ok := s.Get(sessionScope).(string)
if !ok || scope == "" { if !ok || scope == "" {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no scope found in session"}) err := fmt.Errorf("key %s was not found in session", sessionScope)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return return
} }
// the authorize template will display a form to the user where they can get some information // the authorize template will display a form to the user where they can get some information
// about the app that's trying to authorize, and the scope of the request. // about the app that's trying to authorize, and the scope of the request.
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
l.Trace("serving authorize html")
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
"appname": app.Name, "appname": app.Name,
"appwebsite": app.Website, "appwebsite": app.Website,
"redirect": redirect, "redirect": redirect,
sessionScope: scope, "scope": scope,
"user": acct.Username, "user": acct.Username,
}) })
} }
@ -136,13 +158,10 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them, // At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
// so we should proceed with the authentication flow and generate an oauth token for them if we can. // so we should proceed with the authentication flow and generate an oauth token for them if we can.
func (m *Module) AuthorizePOSTHandler(c *gin.Context) { func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "AuthorizePOSTHandler")
s := sessions.Default(c) s := sessions.Default(c)
// We need to retrieve the original form submitted to the authorizeGEThandler, and // We need to retrieve the original form submitted to the authorizeGEThandler, and
// recreate it on the request so that it can be used further by the oauth2 library. // recreate it on the request so that it can be used further by the oauth2 library.
// So first fetch all the values from the session.
errs := []string{} errs := []string{}
forceLogin, ok := s.Get(sessionForceLogin).(string) forceLogin, ok := s.Get(sessionForceLogin).(string)
@ -152,77 +171,107 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
responseType, ok := s.Get(sessionResponseType).(string) responseType, ok := s.Get(sessionResponseType).(string)
if !ok || responseType == "" { if !ok || responseType == "" {
errs = append(errs, "session missing response_type") errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType))
} }
clientID, ok := s.Get(sessionClientID).(string) clientID, ok := s.Get(sessionClientID).(string)
if !ok || clientID == "" { if !ok || clientID == "" {
errs = append(errs, "session missing client_id") errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID))
} }
redirectURI, ok := s.Get(sessionRedirectURI).(string) redirectURI, ok := s.Get(sessionRedirectURI).(string)
if !ok || redirectURI == "" { if !ok || redirectURI == "" {
errs = append(errs, "session missing redirect_uri") errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI))
} }
scope, ok := s.Get(sessionScope).(string) scope, ok := s.Get(sessionScope).(string)
if !ok { if !ok {
errs = append(errs, "session missing scope") errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope))
} }
userID, ok := s.Get(sessionUserID).(string) userID, ok := s.Get(sessionUserID).(string)
if !ok { if !ok {
errs = append(errs, "session missing userid") errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID))
}
if len(errs) != 0 {
errs = append(errs, helpfulAdvice)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet)
return
} }
// redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
// or thier account has been disabled.
user := &gtsmodel.User{} user := &gtsmodel.User{}
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
var errWithCode gtserror.WithCode
if err == db.ErrNoEntries {
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
} else {
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
}
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
if err != nil { if err != nil {
m.clearSession(s) m.clearSession(s)
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
return var errWithCode gtserror.WithCode
} if err == db.ErrNoEntries {
if !ensureUserIsAuthorizedOrRedirect(c, user, acct) { errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
} else {
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
}
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
return
}
// we're done with the session now, so just clear it out
m.clearSession(s) m.clearSession(s)
if len(errs) != 0 { // we have to set the values on the request form
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": strings.Join(errs, ": ")}) // so that they're picked up by the oauth server
return c.Request.Form = url.Values{
sessionForceLogin: {forceLogin},
sessionResponseType: {responseType},
sessionClientID: {clientID},
sessionRedirectURI: {redirectURI},
sessionScope: {scope},
sessionUserID: {userID},
} }
// now set the values on the request if err := m.processor.OAuthHandleAuthorizeRequest(c.Writer, c.Request); err != nil {
values := url.Values{} api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet)
values.Set(sessionForceLogin, forceLogin)
values.Set(sessionResponseType, responseType)
values.Set(sessionClientID, clientID)
values.Set(sessionRedirectURI, redirectURI)
values.Set(sessionScope, scope)
values.Set(sessionUserID, userID)
c.Request.Form = values
l.Tracef("values on request set to %+v", c.Request.Form)
// and proceed with authorization using the oauth2 library
if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil {
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
} }
} }
// extractAuthForm checks the given OAuthAuthorize form, and stores // saveAuthFormToSession checks the given OAuthAuthorize form,
// the values in the form into the session. // and stores the values in the form into the session.
func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error { func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtserror.WithCode {
// these fields are *required* so check 'em if form == nil {
if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { err := errors.New("OAuthAuthorize form was nil")
return errors.New("missing one of: response_type, client_id or redirect_uri") return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
}
if form.ResponseType == "" {
err := errors.New("field response_type was not set on OAuthAuthorize form")
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
}
if form.ClientID == "" {
err := errors.New("field client_id was not set on OAuthAuthorize form")
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
}
if form.RedirectURI == "" {
err := errors.New("field redirect_uri was not set on OAuthAuthorize form")
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
} }
// set default scope to read // set default scope to read
@ -237,29 +286,33 @@ func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
s.Set(sessionRedirectURI, form.RedirectURI) s.Set(sessionRedirectURI, form.RedirectURI)
s.Set(sessionScope, form.Scope) s.Set(sessionScope, form.Scope)
s.Set(sessionState, uuid.NewString()) s.Set(sessionState, uuid.NewString())
return s.Save()
if err := s.Save(); err != nil {
err := fmt.Errorf("error saving form values onto session: %s", err)
return gtserror.NewErrorInternalError(err, helpfulAdvice)
}
return nil
} }
func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) bool { func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) {
if user.ConfirmedAt.IsZero() { if user.ConfirmedAt.IsZero() {
ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath) ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath)
return false redirected = true
return
} }
if !user.Approved { if !user.Approved {
ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath) ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath)
return false redirected = true
return
} }
if user.Disabled { if user.Disabled || !account.SuspendedAt.IsZero() {
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
return false redirected = true
return
} }
if !account.SuspendedAt.IsZero() { return
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
return false
}
return true
} }

View file

@ -69,7 +69,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
} }
doTest := func(testCase authorizeHandlerTestCase) { doTest := func(testCase authorizeHandlerTestCase) {
ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath) ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath, nil, "")
user := suite.testUsers["unconfirmed_account"] user := suite.testUsers["unconfirmed_account"]
account := suite.testAccounts["unconfirmed_account"] account := suite.testAccounts["unconfirmed_account"]

View file

@ -30,7 +30,9 @@
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/oidc"
"github.com/superseriousbusiness/gotosocial/internal/validate" "github.com/superseriousbusiness/gotosocial/internal/validate"
@ -40,11 +42,14 @@
func (m *Module) CallbackGETHandler(c *gin.Context) { func (m *Module) CallbackGETHandler(c *gin.Context) {
s := sessions.Default(c) s := sessions.Default(c)
// first make sure the state set in the cookie is the same as the state returned from the external provider // check the query vs session state parameter to mitigate csrf
// https://auth0.com/docs/secure/attack-protection/state-parameters
state := c.Query(callbackStateParam) state := c.Query(callbackStateParam)
if state == "" { if state == "" {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"}) err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -52,84 +57,104 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
savedState, ok := savedStateI.(string) savedState, ok := savedStateI.(string)
if !ok { if !ok {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"}) err := fmt.Errorf("key %s was not found in session", sessionState)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
if state != savedState { if state != savedState {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"}) err := errors.New("mismatch between query state and session state")
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
// retrieve stored claims using code
code := c.Query(callbackCodeParam) code := c.Query(callbackCodeParam)
if code == "" {
claims, err := m.idp.HandleCallback(c.Request.Context(), code)
if err != nil {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
// We can use the client_id on the session to retrieve info about the app associated with the client_id claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code)
if errWithCode != nil {
m.clearSession(s)
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
// We can use the client_id on the session to retrieve
// info about the app associated with the client_id
clientID, ok := s.Get(sessionClientID).(string) clientID, ok := s.Get(sessionClientID).(string)
if !ok || clientID == "" { if !ok || clientID == "" {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session during callback"}) err := fmt.Errorf("key %s was not found in session", sessionClientID)
return api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
}
app := &gtsmodel.Application{}
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
m.clearSession(s)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
return return
} }
user, err := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID) app := &gtsmodel.Application{}
if err != nil { if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
var errWithCode gtserror.WithCode
if err == db.ErrNoEntries {
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
} else {
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
}
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
user, errWithCode := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
if errWithCode != nil {
m.clearSession(s)
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
s.Set(sessionUserID, user.ID) s.Set(sessionUserID, user.ID)
if err := s.Save(); err != nil { if err := s.Save(); err != nil {
m.clearSession(s) m.clearSession(s)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return return
} }
c.Redirect(http.StatusFound, OauthAuthorizePath) c.Redirect(http.StatusFound, OauthAuthorizePath)
} }
func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, error) { func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
if claims.Email == "" { if claims.Email == "" {
return nil, errors.New("no email returned in claims") err := errors.New("no email returned in claims")
return nil, gtserror.NewErrorBadRequest(err, err.Error())
} }
// see if we already have a user for this email address // see if we already have a user for this email address
// if so, we don't need to continue + create one
user := &gtsmodel.User{} user := &gtsmodel.User{}
err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user) err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user)
if err == nil { if err == nil {
// we do! so we can just return it
return user, nil return user, nil
} }
if err != db.ErrNoEntries { if err != db.ErrNoEntries {
// we have an actual error in the database err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) return nil, gtserror.NewErrorInternalError(err)
} }
// maybe we have an unconfirmed user // maybe we have an unconfirmed user
err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user) err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user)
if err == nil { if err == nil {
// user is unconfirmed so return an error err := fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email) return nil, gtserror.NewErrorForbidden(err, err.Error())
} }
if err != db.ErrNoEntries { if err != db.ErrNoEntries {
// we have an actual error in the database err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) return nil, gtserror.NewErrorInternalError(err)
} }
// we don't have a confirmed or unconfirmed user with the claimed email address // we don't have a confirmed or unconfirmed user with the claimed email address
@ -138,10 +163,10 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// check if the email address is available for use; if it's not there's nothing we can so // check if the email address is available for use; if it's not there's nothing we can so
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email) emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
if err != nil { if err != nil {
return nil, fmt.Errorf("email %s not available: %s", claims.Email, err) return nil, gtserror.NewErrorBadRequest(err)
} }
if !emailAvailable { if !emailAvailable {
return nil, fmt.Errorf("email %s in use", claims.Email) return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email))
} }
// now we need a username // now we need a username
@ -149,12 +174,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// make sure claims.Name is defined since we'll be using that for the username // make sure claims.Name is defined since we'll be using that for the username
if claims.Name == "" { if claims.Name == "" {
return nil, errors.New("no name returned in claims") err := errors.New("no name returned in claims")
return nil, gtserror.NewErrorBadRequest(err, err.Error())
} }
// check if we can just use claims.Name as-is // check if we can just use claims.Name as-is
err = validate.Username(claims.Name) if err = validate.Username(claims.Name); err == nil {
if err == nil {
// the name we have on the claims is already a valid username // the name we have on the claims is already a valid username
username = claims.Name username = claims.Name
} else { } else {
@ -166,12 +191,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// lowercase the whole thing // lowercase the whole thing
lower := strings.ToLower(underscored) lower := strings.ToLower(underscored)
// see if this is valid.... // see if this is valid....
if err := validate.Username(lower); err == nil { if err := validate.Username(lower); err != nil {
// we managed to get a valid username err := fmt.Errorf("couldn't parse a valid username from claims.Name value of %s: %s", claims.Name, err)
username = lower return nil, gtserror.NewErrorBadRequest(err, err.Error())
} else {
return nil, fmt.Errorf("couldn't parse a valid username from claims.Name value of %s", claims.Name)
} }
// we managed to get a valid username
username = lower
} }
var iString string var iString string
@ -185,7 +210,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
for i := 1; !found; i++ { for i := 1; !found; i++ {
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString) usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
if err != nil { if err != nil {
return nil, err return nil, gtserror.NewErrorInternalError(err)
} }
if usernameAvailable { if usernameAvailable {
// no error so we've found a username that works // no error so we've found a username that works
@ -223,7 +248,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// create the user! this will also create an account and store it in the database so we don't need to do that here // create the user! this will also create an account and store it in the database so we don't need to do that here
user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin) user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating user: %s", err) return nil, gtserror.NewErrorInternalError(err)
} }
return user, nil return user, nil

View file

@ -21,14 +21,14 @@
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -41,64 +41,62 @@ type login struct {
// SignInGETHandler should be served at https://example.org/auth/sign_in. // SignInGETHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password. // The idea is to present a sign in page to the user, where they can enter their username and password.
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler.
// If an idp provider is set, then the user will be redirected to that to do their sign in.
func (m *Module) SignInGETHandler(c *gin.Context) { func (m *Module) SignInGETHandler(c *gin.Context) {
l := logrus.WithField("func", "SignInGETHandler")
l.Trace("entering sign in handler")
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
if m.idp != nil { if m.idp == nil {
s := sessions.Default(c) // no idp provider, use our own funky little sign in page
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
stateI := s.Get(sessionState)
state, ok := stateI.(string)
if !ok {
m.clearSession(s)
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
return
}
redirect := m.idp.AuthCodeURL(state)
l.Debugf("redirecting to external idp at %s", redirect)
c.Redirect(http.StatusSeeOther, redirect)
return return
} }
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
// idp provider is in use, so redirect to it
s := sessions.Default(c)
stateI := s.Get(sessionState)
state, ok := stateI.(string)
if !ok {
m.clearSession(s)
err := fmt.Errorf("key %s was not found in session", sessionState)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(state))
} }
// SignInPOSTHandler should be served at https://example.org/auth/sign_in. // SignInPOSTHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password. // The idea is to present a sign in page to the user, where they can enter their username and password.
// The handler will then redirect to the auth handler served at /auth // The handler will then redirect to the auth handler served at /auth
func (m *Module) SignInPOSTHandler(c *gin.Context) { func (m *Module) SignInPOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "SignInPOSTHandler")
s := sessions.Default(c) s := sessions.Default(c)
form := &login{} form := &login{}
if err := c.ShouldBind(form); err != nil { if err := c.ShouldBind(form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
m.clearSession(s) m.clearSession(s)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return return
} }
l.Tracef("parsed form: %+v", form)
userid, err := m.ValidatePassword(c.Request.Context(), form.Email, form.Password) userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
if err != nil { if errWithCode != nil {
c.String(http.StatusForbidden, err.Error()) // don't clear session here, so the user can just press back and try again
m.clearSession(s) // if they accidentally gave the wrong password or something
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
s.Set(sessionUserID, userid) s.Set(sessionUserID, userid)
if err := s.Save(); err != nil { if err := s.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) err := fmt.Errorf("error saving user id onto session: %s", err)
m.clearSession(s) api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet)
return
} }
l.Trace("redirecting to auth page")
c.Redirect(http.StatusFound, OauthAuthorizePath) c.Redirect(http.StatusFound, OauthAuthorizePath)
} }
@ -106,42 +104,34 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
// The goal is to authenticate the password against the one for that email // The goal is to authenticate the password against the one for that email
// address stored in the database. If OK, we return the userid (a ulid) for that user, // address stored in the database. If OK, we return the userid (a ulid) for that user,
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (userid string, err error) { func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) {
l := logrus.WithField("func", "ValidatePassword")
// make sure an email/password was provided and bail if not
if email == "" || password == "" { if email == "" || password == "" {
l.Debug("email or password was not provided") err := errors.New("email or password was not provided")
return incorrectPassword() return incorrectPassword(err)
} }
// first we select the user from the database based on email address, bail if no user found for that email user := &gtsmodel.User{}
gtsUser := &gtsmodel.User{} if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, user); err != nil {
err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, gtsUser); err != nil { return incorrectPassword(err)
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
return incorrectPassword()
} }
// make sure a password is actually set and bail if not if user.EncryptedPassword == "" {
if gtsUser.EncryptedPassword == "" { err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email)
l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) return incorrectPassword(err)
return incorrectPassword()
} }
// compare the provided password with the encrypted one from the db, bail if they don't match if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil { err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) return incorrectPassword(err)
return incorrectPassword()
} }
// If we've made it this far the email/password is correct, so we can just return the id of the user. return user.ID, nil
userid = gtsUser.ID
l.Tracef("returning (%s, %s)", userid, err)
return
} }
// incorrectPassword is just a little helper function to use in the ValidatePassword function // incorrectPassword wraps the given error in a gtserror.WithCode, and returns
func incorrectPassword() (string, error) { // only a generic 'safe' error message to the user, to not give any info away.
return "", errors.New("password/email combination was incorrect") func incorrectPassword(err error) (string, gtserror.WithCode) {
safeErr := fmt.Errorf("password/email combination was incorrect")
return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), helpfulAdvice)
} }

View file

@ -22,56 +22,94 @@
"net/http" "net/http"
"net/url" "net/url"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type tokenBody struct { type tokenRequestForm struct {
GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"`
Code *string `form:"code" json:"code" xml:"code"`
RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"`
ClientID *string `form:"client_id" json:"client_id" xml:"client_id"` ClientID *string `form:"client_id" json:"client_id" xml:"client_id"`
ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"` ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"`
Code *string `form:"code" json:"code" xml:"code"`
GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"`
RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"`
Scope *string `form:"scope" json:"scope" xml:"scope"` Scope *string `form:"scope" json:"scope" xml:"scope"`
} }
// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
func (m *Module) TokenPOSTHandler(c *gin.Context) { func (m *Module) TokenPOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "TokenPOSTHandler")
l.Trace("entered TokenPOSTHandler")
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
form := &tokenBody{} help := []string{}
if err := c.ShouldBind(form); err == nil {
c.Request.Form = url.Values{} form := &tokenRequestForm{}
if form.ClientID != nil { if err := c.ShouldBind(form); err != nil {
c.Request.Form.Set("client_id", *form.ClientID) api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), err.Error()))
} return
if form.ClientSecret != nil {
c.Request.Form.Set("client_secret", *form.ClientSecret)
}
if form.Code != nil {
c.Request.Form.Set("code", *form.Code)
}
if form.GrantType != nil {
c.Request.Form.Set("grant_type", *form.GrantType)
}
if form.RedirectURI != nil {
c.Request.Form.Set("redirect_uri", *form.RedirectURI)
}
if form.Scope != nil {
c.Request.Form.Set("scope", *form.Scope)
}
} }
if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { c.Request.Form = url.Values{}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
var grantType string
if form.GrantType != nil {
grantType = *form.GrantType
c.Request.Form.Set("grant_type", grantType)
} else {
help = append(help, "grant_type was not set in the token request form, but must be set to authorization_code or client_credentials")
} }
if form.ClientID != nil {
c.Request.Form.Set("client_id", *form.ClientID)
} else {
help = append(help, "client_id was not set in the token request form")
}
if form.ClientSecret != nil {
c.Request.Form.Set("client_secret", *form.ClientSecret)
} else {
help = append(help, "client_secret was not set in the token request form")
}
if form.RedirectURI != nil {
c.Request.Form.Set("redirect_uri", *form.RedirectURI)
} else {
help = append(help, "redirect_uri was not set in the token request form")
}
var code string
if form.Code != nil {
if grantType != "authorization_code" {
help = append(help, "a code was provided in the token request form, but grant_type was not set to authorization_code")
} else {
code = *form.Code
c.Request.Form.Set("code", code)
}
} else if grantType == "authorization_code" {
help = append(help, "code was not set in the token request form, but must be set since grant_type is authorization_code")
}
if form.Scope != nil {
c.Request.Form.Set("scope", *form.Scope)
}
if len(help) != 0 {
api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), help...))
return
}
token, errWithCode := m.processor.OAuthHandleTokenRequest(c.Request)
if errWithCode != nil {
api.OAuthErrorHandler(c, errWithCode)
return
}
c.Header("Cache-Control", "no-store")
c.Header("Pragma", "no-cache")
c.JSON(http.StatusOK, token)
} }

View file

@ -0,0 +1,215 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth_test
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type TokenTestSuite struct {
AuthStandardTestSuite
}
func (suite *TokenTestSuite) TestPOSTTokenEmptyForm() {
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", []byte{}, "")
ctx.Request.Header.Set("accept", "application/json")
suite.authModule.TokenPOSTHandler(ctx)
suite.Equal(http.StatusBadRequest, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: grant_type was not set in the token request form, but must be set to authorization_code or client_credentials: client_id was not set in the token request form: client_secret was not set in the token request form: redirect_uri was not set in the token request form"}`, string(b))
}
func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() {
testClient := suite.testClients["local_account_1"]
requestBody, w, err := testrig.CreateMultipartFormData(
"", "",
map[string]string{
"grant_type": "client_credentials",
"client_id": testClient.ID,
"client_secret": testClient.Secret,
"redirect_uri": "http://localhost:8080",
})
if err != nil {
panic(err)
}
bodyBytes := requestBody.Bytes()
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
ctx.Request.Header.Set("accept", "application/json")
suite.authModule.TokenPOSTHandler(ctx)
suite.Equal(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
t := &apimodel.Token{}
err = json.Unmarshal(b, t)
suite.NoError(err)
suite.Equal("Bearer", t.TokenType)
suite.NotEmpty(t.AccessToken)
suite.NotEmpty(t.CreatedAt)
suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute)
// there should be a token in the database now too
dbToken := &gtsmodel.Token{}
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken)
suite.NoError(err)
suite.NotNil(dbToken)
}
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() {
testClient := suite.testClients["local_account_1"]
testUserAuthorizationToken := suite.testTokens["local_account_1_user_authorization_token"]
requestBody, w, err := testrig.CreateMultipartFormData(
"", "",
map[string]string{
"grant_type": "authorization_code",
"client_id": testClient.ID,
"client_secret": testClient.Secret,
"redirect_uri": "http://localhost:8080",
"code": testUserAuthorizationToken.Code,
})
if err != nil {
panic(err)
}
bodyBytes := requestBody.Bytes()
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
ctx.Request.Header.Set("accept", "application/json")
suite.authModule.TokenPOSTHandler(ctx)
suite.Equal(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
t := &apimodel.Token{}
err = json.Unmarshal(b, t)
suite.NoError(err)
suite.Equal("Bearer", t.TokenType)
suite.NotEmpty(t.AccessToken)
suite.NotEmpty(t.CreatedAt)
suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute)
dbToken := &gtsmodel.Token{}
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken)
suite.NoError(err)
suite.NotNil(dbToken)
}
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() {
testClient := suite.testClients["local_account_1"]
requestBody, w, err := testrig.CreateMultipartFormData(
"", "",
map[string]string{
"grant_type": "authorization_code",
"client_id": testClient.ID,
"client_secret": testClient.Secret,
"redirect_uri": "http://localhost:8080",
})
if err != nil {
panic(err)
}
bodyBytes := requestBody.Bytes()
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
ctx.Request.Header.Set("accept", "application/json")
suite.authModule.TokenPOSTHandler(ctx)
suite.Equal(http.StatusBadRequest, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: code was not set in the token request form, but must be set since grant_type is authorization_code"}`, string(b))
}
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() {
testClient := suite.testClients["local_account_1"]
requestBody, w, err := testrig.CreateMultipartFormData(
"", "",
map[string]string{
"grant_type": "client_credentials",
"client_id": testClient.ID,
"client_secret": testClient.Secret,
"redirect_uri": "http://localhost:8080",
"code": "peepeepoopoo",
})
if err != nil {
panic(err)
}
bodyBytes := requestBody.Bytes()
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
ctx.Request.Header.Set("accept", "application/json")
suite.authModule.TokenPOSTHandler(ctx)
suite.Equal(http.StatusBadRequest, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: a code was provided in the token request form, but grant_type was not set to authorization_code"}`, string(b))
}
func TestTokenTestSuite(t *testing.T) {
suite.Run(t, &TokenTestSuite{})
}

View file

@ -19,13 +19,13 @@
package blocks package blocks
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -80,24 +80,25 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/account" // "$ref": "#/definitions/account"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) BlocksGETHandler(c *gin.Context) { func (m *Module) BlocksGETHandler(c *gin.Context) {
l := logrus.WithField("func", "PublicTimelineGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -118,8 +119,8 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -127,8 +128,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit) resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error from processor BlocksGet: %s", errWithCode) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -5,18 +5,25 @@
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// EmojisGETHandler returns a list of custom emojis enabled on the instance // EmojisGETHandler returns a list of custom emojis enabled on the instance
func (m *Module) EmojisGETHandler(c *gin.Context) { func (m *Module) EmojisGETHandler(c *gin.Context) {
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
emojis, errWithCode := m.processor.CustomEmojisGet(c) emojis, errWithCode := m.processor.CustomEmojisGet(c)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -1,29 +1,26 @@
package favourites package favourites
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// FavouritesGETHandler handles GETting favourites. // FavouritesGETHandler handles GETting favourites.
func (m *Module) FavouritesGETHandler(c *gin.Context) { func (m *Module) FavouritesGETHandler(c *gin.Context) {
l := logrus.WithField("func", "PublicTimelineGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -44,8 +41,8 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -53,13 +50,12 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit) resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error from processor FavedTimelineGet: %s", errWithCode) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
if resp.LinkHeader != "" { if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader) c.Header("Link", resp.LinkHeader)
} }
c.JSON(http.StatusOK, resp.Statuses) c.JSON(http.StatusOK, resp.Items)
} }

View file

@ -19,6 +19,7 @@
package fileserver package fileserver
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
@ -26,6 +27,7 @@
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -34,17 +36,9 @@
// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found". // Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything. // Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
func (m *FileServer) ServeFile(c *gin.Context) { func (m *FileServer) ServeFile(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "ServeFile",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Trace("received request")
authed, err := oauth.Authed(c, false, false, false, false) authed, err := oauth.Authed(c, false, false, false, false)
if err != nil { if err != nil {
c.String(http.StatusNotFound, "404 page not found") api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return return
} }
@ -53,29 +47,29 @@ func (m *FileServer) ServeFile(c *gin.Context) {
// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. // "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
accountID := c.Param(AccountIDKey) accountID := c.Param(AccountIDKey)
if accountID == "" { if accountID == "" {
l.Debug("missing accountID from request") err := fmt.Errorf("missing %s from request", AccountIDKey)
c.String(http.StatusNotFound, "404 page not found") api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return return
} }
mediaType := c.Param(MediaTypeKey) mediaType := c.Param(MediaTypeKey)
if mediaType == "" { if mediaType == "" {
l.Debug("missing mediaType from request") err := fmt.Errorf("missing %s from request", MediaTypeKey)
c.String(http.StatusNotFound, "404 page not found") api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return return
} }
mediaSize := c.Param(MediaSizeKey) mediaSize := c.Param(MediaSizeKey)
if mediaSize == "" { if mediaSize == "" {
l.Debug("missing mediaSize from request") err := fmt.Errorf("missing %s from request", MediaSizeKey)
c.String(http.StatusNotFound, "404 page not found") api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return return
} }
fileName := c.Param(FileNameKey) fileName := c.Param(FileNameKey)
if fileName == "" { if fileName == "" {
l.Debug("missing fileName from request") err := fmt.Errorf("missing %s from request", FileNameKey)
c.String(http.StatusNotFound, "404 page not found") api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return return
} }
@ -86,8 +80,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
FileName: fileName, FileName: fileName,
}) })
if errWithCode != nil { if errWithCode != nil {
l.Errorf(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
@ -95,7 +88,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
// if the content is a ReadCloser, close it when we're done // if the content is a ReadCloser, close it when we're done
if closer, ok := content.Content.(io.ReadCloser); ok { if closer, ok := content.Content.(io.ReadCloser); ok {
if err := closer.Close(); err != nil { if err := closer.Close(); err != nil {
l.Errorf("error closing readcloser: %s", err) logrus.Errorf("ServeFile: error closing readcloser: %s", err)
} }
} }
}() }()
@ -103,9 +96,9 @@ func (m *FileServer) ServeFile(c *gin.Context) {
// TODO: if the requester only accepts text/html we should try to serve them *something*. // TODO: if the requester only accepts text/html we should try to serve them *something*.
// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will // This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will
// attempt to look up the content to provide a preview of the link, and they ask for text/html. // attempt to look up the content to provide a preview of the link, and they ask for text/html.
format, err := api.NegotiateAccept(c, api.Offer(content.ContentType)) format, err := api.NegotiateAccept(c, api.MIME(content.ContentType))
if errWithCode != nil { if err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }

View file

@ -5,12 +5,19 @@
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// FiltersGETHandler returns a list of filters set by/for the authed account // FiltersGETHandler returns a list of filters set by/for the authed account
func (m *Module) FiltersGETHandler(c *gin.Context) { func (m *Module) FiltersGETHandler(c *gin.Context) {
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }

View file

@ -19,12 +19,12 @@
package followrequest package followrequest
import ( import (
"errors"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -62,43 +62,34 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500': // '500':
// description: internal server error // description: internal server error
func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
l.Debugf("couldn't auth: %s", err)
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
return return
} }
originAccountID := c.Param(IDKey) originAccountID := c.Param(IDKey)
if originAccountID == "" { if originAccountID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID)
if errWithCode != nil { if errWithCode != nil {
l.Debug(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -82,6 +82,34 @@ func (suite *AuthorizeTestSuite) TestAuthorize() {
suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b)) suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b))
} }
func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() {
requestingAccount := suite.testAccounts["remote_account_2"]
recorder := httptest.NewRecorder()
ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "")
ctx.Params = gin.Params{
gin.Param{
Key: followrequest.IDKey,
Value: requestingAccount.ID,
},
}
// call the handler
suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx)
suite.Equal(http.StatusNotFound, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
// check the response
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
suite.Equal(`{"error":"Not Found"}`, string(b))
}
func TestAuthorizeTestSuite(t *testing.T) { func TestAuthorizeTestSuite(t *testing.T) {
suite.Run(t, &AuthorizeTestSuite{}) suite.Run(t, &AuthorizeTestSuite{})
} }

View file

@ -21,10 +21,9 @@
import ( import (
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -71,34 +70,27 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) FollowRequestGETHandler(c *gin.Context) { func (m *Module) FollowRequestGETHandler(c *gin.Context) {
l := logrus.WithField("func", "FollowRequestGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
l.Debugf("couldn't auth: %s", err)
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
return return
} }
accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed) accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -70,7 +70,7 @@ func (suite *GetTestSuite) TestGet() {
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.00Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b)) suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
} }
func TestGetTestSuite(t *testing.T) { func TestGetTestSuite(t *testing.T) {

View file

@ -19,11 +19,12 @@
package followrequest package followrequest
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -59,43 +60,34 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500': // '500':
// description: internal server error // description: internal server error
func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "FollowRequestRejectPOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
l.Debugf("couldn't auth: %s", err)
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
return return
} }
originAccountID := c.Param(IDKey) originAccountID := c.Param(IDKey)
if originAccountID == "" { if originAccountID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) err := errors.New("no account id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID) relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID)
if errWithCode != nil { if errWithCode != nil {
l.Debug(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -3,9 +3,9 @@
import ( import (
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -30,22 +30,19 @@
// description: "Instance information." // description: "Instance information."
// schema: // schema:
// "$ref": "#/definitions/instance" // "$ref": "#/definitions/instance"
// '406':
// description: not acceptable
// '500': // '500':
// description: internal error // description: internal error
func (m *Module) InstanceInformationGETHandler(c *gin.Context) { func (m *Module) InstanceInformationGETHandler(c *gin.Context) {
l := logrus.WithField("func", "InstanceInformationGETHandler")
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
host := config.GetHost() instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost())
if errWithCode != nil {
instance, err := m.processor.InstanceGet(c.Request.Context(), host) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
if err != nil {
l.Debugf("error getting instance from processor: %s", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return return
} }

View file

@ -1,13 +1,13 @@
package instance package instance
import ( import (
"errors"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -82,52 +82,51 @@
// description: "The newly updated instance." // description: "The newly updated instance."
// schema: // schema:
// "$ref": "#/definitions/instance" // "$ref": "#/definitions/instance"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
l := logrus.WithField("func", "InstanceUpdatePATCHHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
// only admins can update instance settings
if !authed.User.Admin { if !authed.User.Admin {
l.Debug("user is not an admin so cannot update instance settings") err := errors.New("user is not an admin so cannot update instance settings")
c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"}) api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return return
} }
l.Debug("parsing request form")
form := &model.InstanceSettingsUpdateRequest{} form := &model.InstanceSettingsUpdateRequest{}
if err := c.ShouldBind(&form); err != nil || form == nil { if err := c.ShouldBind(&form); err != nil {
l.Debugf("could not parse form from request: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
l.Debugf("parsed form: %+v", form)
// if everything on the form is nil, then nothing has been set and we shouldn't continue
if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil { if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil {
l.Debugf("could not parse form from request") err := errors.New("empty form submitted")
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form) i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error with instance patch request: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -26,6 +26,7 @@
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance" "github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -62,7 +63,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
b, err := io.ReadAll(result.Body) b, err := io.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.00Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.00Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
} }
func (suite *InstancePatchTestSuite) TestInstancePatch2() { func (suite *InstancePatchTestSuite) TestInstancePatch2() {
@ -125,6 +126,67 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b))
} }
func (suite *InstancePatchTestSuite) TestInstancePatch4() {
requestBody, w, err := testrig.CreateMultipartFormData(
"", "",
map[string]string{})
if err != nil {
panic(err)
}
bodyBytes := requestBody.Bytes()
// set up the request
recorder := httptest.NewRecorder()
ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
// call the handler
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
suite.Equal(http.StatusBadRequest, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
}
func (suite *InstancePatchTestSuite) TestInstancePatch5() {
requestBody, w, err := testrig.CreateMultipartFormData(
"", "",
map[string]string{
"short_description": "<p>This is some html, which is <em>allowed</em> in short descriptions.</p>",
})
if err != nil {
panic(err)
}
bodyBytes := requestBody.Bytes()
// set up the request
recorder := httptest.NewRecorder()
ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
// call the handler
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
suite.Equal(http.StatusForbidden, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"Forbidden: user is not an admin so cannot update instance settings"}`, string(b))
}
func TestInstancePatchTestSuite(t *testing.T) { func TestInstancePatchTestSuite(t *testing.T) {
suite.Run(t, &InstancePatchTestSuite{}) suite.Run(t, &InstancePatchTestSuite{})
} }

View file

@ -5,12 +5,19 @@
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// ListsGETHandler returns a list of lists created by/for the authed account // ListsGETHandler returns a list of lists created by/for the authed account
func (m *Module) ListsGETHandler(c *gin.Context) { func (m *Module) ListsGETHandler(c *gin.Context) {
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }

View file

@ -23,12 +23,11 @@
"fmt" "fmt"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -80,46 +79,36 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403':
// description: forbidden
// '422': // '422':
// description: unprocessable // description: unprocessable
// '500':
// description: internal server error
func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "statusCreatePOSTHandler") authed, err := oauth.Authed(c, true, true, true, true)
authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything*
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
// extract the media create form from the request context
l.Tracef("parsing request form: %s", c.Request.Form)
form := &model.AttachmentRequest{} form := &model.AttachmentRequest{}
if err := c.ShouldBind(&form); err != nil { if err := c.ShouldBind(&form); err != nil {
l.Debugf("error parsing form: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)})
return return
} }
// Give the fields on the request form a first pass to make sure the request is superficially valid.
l.Tracef("validating form %+v", form)
if err := validateCreateMedia(form); err != nil { if err := validateCreateMedia(form); err != nil {
l.Debugf("error validating form: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return return
} }
l.Debug("calling processor media create func") apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form)
apiAttachment, err := m.processor.MediaCreate(c.Request.Context(), authed, form)
if err != nil { if err != nil {
l.Debugf("error creating attachment: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return return
} }
@ -143,6 +132,7 @@ func validateCreateMedia(form *model.AttachmentRequest) error {
if maxImageSize > maxSize { if maxImageSize > maxSize {
maxSize = maxImageSize maxSize = maxImageSize
} }
if form.File.Size > int64(maxSize) { if form.File.Size > int64(maxSize) {
return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
} }

View file

@ -247,15 +247,14 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() {
suite.mediaModule.MediaCreatePOSTHandler(ctx) suite.mediaModule.MediaCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusUnprocessableEntity, recorder.Code) suite.EqualValues(http.StatusBadRequest, recorder.Code)
result := recorder.Result() result := recorder.Result()
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
expectedErr := fmt.Sprintf(`{"error":"image description length must be between 0 and 500 characters (inclusive), but provided image description was %d chars"}`, len(description)) suite.Equal(`{"error":"Bad Request: image description length must be between 0 and 500 characters (inclusive), but provided image description was 6667 chars"}`, string(b))
suite.Equal(expectedErr, string(b))
} }
func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() {

View file

@ -19,12 +19,12 @@
package media package media
import ( import (
"errors"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -59,33 +59,34 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403': // '404':
// description: forbidden // description: not found
// '422': // '406':
// description: unprocessable // description: not acceptable
// '500':
// description: internal server error
func (m *Module) MediaGETHandler(c *gin.Context) { func (m *Module) MediaGETHandler(c *gin.Context) {
l := logrus.WithField("func", "MediaGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
attachmentID := c.Param(IDKey) attachmentID := c.Param(IDKey)
if attachmentID == "" { if attachmentID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) err := errors.New("no attachment id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID) attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -23,12 +23,11 @@
"fmt" "fmt"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -89,50 +88,45 @@
// description: bad request // description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403': // '404':
// description: forbidden // description: not found
// '422': // '406':
// description: unprocessable // description: not acceptable
// '500':
// description: internal server error
func (m *Module) MediaPUTHandler(c *gin.Context) { func (m *Module) MediaPUTHandler(c *gin.Context) {
l := logrus.WithField("func", "MediaGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
attachmentID := c.Param(IDKey) attachmentID := c.Param(IDKey)
if attachmentID == "" { if attachmentID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) err := errors.New("no attachment id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
// extract the media update form from the request context form := &model.AttachmentUpdateRequest{}
l.Tracef("parsing request form: %s", c.Request.Form) if err := c.ShouldBind(form); err != nil {
var form model.AttachmentUpdateRequest api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
if err := c.ShouldBind(&form); err != nil {
l.Debugf("could not parse form from request: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
return return
} }
// Give the fields on the request form a first pass to make sure the request is superficially valid. if err := validateUpdateMedia(form); err != nil {
l.Tracef("validating form %+v", form) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
if err := validateUpdateMedia(&form); err != nil {
l.Debugf("error validating form: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, &form) attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }

View file

@ -232,7 +232,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() {
suite.NoError(err) suite.NoError(err)
// reply should be an error message // reply should be an error message
suite.Equal(`{"error":"image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b)) suite.Equal(`{"error":"Bad Request: image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
} }
func TestMediaUpdateTestSuite(t *testing.T) { func TestMediaUpdateTestSuite(t *testing.T) {

View file

@ -19,34 +19,26 @@
package notification package notification
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
// NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters // NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters
func (m *Module) NotificationsGETHandler(c *gin.Context) { func (m *Module) NotificationsGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ authed, err := oauth.Authed(c, true, true, true, true)
"func": "NotificationsGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
if err != nil { if err != nil {
l.Errorf("error authing status faved by request: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -55,8 +47,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -74,12 +66,14 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
sinceID = sinceIDString sinceID = sinceIDString
} }
notifs, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID) resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error processing notifications get: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
c.JSON(http.StatusOK, notifs) if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
c.JSON(http.StatusOK, resp.Items)
} }

View file

@ -19,14 +19,15 @@
package search package search
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -52,50 +53,44 @@
// type: array // type: array
// items: // items:
// "$ref": "#/definitions/searchResult" // "$ref": "#/definitions/searchResult"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) SearchGETHandler(c *gin.Context) { func (m *Module) SearchGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ authed, err := oauth.Authed(c, true, true, true, true)
"func": "SearchGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
if err != nil { if err != nil {
l.Errorf("error authing search request: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
accountID := c.Query(AccountIDKey)
maxID := c.Query(MaxIDKey)
minID := c.Query(MinIDKey)
searchType := c.Query(TypeKey)
excludeUnreviewed := false excludeUnreviewed := false
excludeUnreviewedString := c.Query(ExcludeUnreviewedKey) excludeUnreviewedString := c.Query(ExcludeUnreviewedKey)
if excludeUnreviewedString != "" { if excludeUnreviewedString != "" {
var err error var err error
excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString) excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", excludeUnreviewedString, err)}) err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
} }
query := c.Query(QueryKey) query := c.Query(QueryKey)
if query == "" { if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter q was empty"}) err := errors.New("query parameter q was empty")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -105,18 +100,19 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
var err error var err error
resolve, err = strconv.ParseBool(resolveString) resolve, err = strconv.ParseBool(resolveString)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", resolveString, err)}) err := fmt.Errorf("error parsing %s: %s", ResolveKey, err)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
} }
limit := 20 limit := 2
limitString := c.Query(LimitKey) limitString := c.Query(LimitKey)
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -133,18 +129,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
if offsetString != "" { if offsetString != "" {
i, err := strconv.ParseInt(offsetString, 10, 64) i, err := strconv.ParseInt(offsetString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing offset string: %s", err) err := fmt.Errorf("error parsing %s: %s", OffsetKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse offset query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
offset = int(i) offset = int(i)
} }
if limit > 40 {
limit = 40
}
if limit < 1 {
limit = 1
}
following := false following := false
followingString := c.Query(FollowingKey) followingString := c.Query(FollowingKey)
@ -152,16 +142,17 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
var err error var err error
following, err = strconv.ParseBool(followingString) following, err = strconv.ParseBool(followingString)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", followingString, err)}) err := fmt.Errorf("error parsing %s: %s", FollowingKey, err)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
} }
searchQuery := &model.SearchQuery{ searchQuery := &model.SearchQuery{
AccountID: accountID, AccountID: c.Query(AccountIDKey),
MaxID: maxID, MaxID: c.Query(MaxIDKey),
MinID: minID, MinID: c.Query(MinIDKey),
Type: searchType, Type: c.Query(TypeKey),
ExcludeUnreviewed: excludeUnreviewed, ExcludeUnreviewed: excludeUnreviewed,
Query: query, Query: query,
Resolve: resolve, Resolve: resolve,
@ -172,8 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery) results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error searching: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -66,37 +67,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { func (m *Module) StatusBoostPOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "StatusBoostPOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debug("not authed so can't boost status") api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID) apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error processing status boost: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -134,13 +134,13 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() {
suite.statusModule.StatusBoostPOSTHandler(ctx) suite.statusModule.StatusBoostPOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses suite.Equal(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
result := recorder.Result() result := recorder.Result()
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
assert.Equal(suite.T(), `{"error":"forbidden"}`, string(b)) assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
} }
// try to boost a status that's not visible to the user // try to boost a status that's not visible to the user
@ -177,13 +177,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() {
suite.statusModule.StatusBoostPOSTHandler(ctx) suite.statusModule.StatusBoostPOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), `{"error":"404 not found"}`, string(b))
} }
func TestStatusBoostTestSuite(t *testing.T) { func TestStatusBoostTestSuite(t *testing.T) {

View file

@ -23,6 +23,7 @@
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -84,10 +85,9 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) {
return return
} }
apiAccounts, err := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status boosted by request: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return return
} }

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -65,37 +66,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusContextGETHandler(c *gin.Context) { func (m *Module) StatusContextGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "StatusContextGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Errorf("error authing status context request: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID) statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error getting status context: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -23,12 +23,11 @@
"fmt" "fmt"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate" "github.com/superseriousbusiness/gotosocial/internal/validate"
) )
@ -61,58 +60,44 @@
// description: "The newly created status." // description: "The newly created status."
// schema: // schema:
// "$ref": "#/definitions/status" // "$ref": "#/definitions/status"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500': // '500':
// description: internal error // description: internal server error
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "statusCreatePOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("couldn't auth: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
// First check this user/account is permitted to post new statuses.
// There's no point continuing otherwise.
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
l.Debugf("couldn't auth: %s", err)
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
return
}
// extract the status create form from the request context
l.Debugf("parsing request form: %s", c.Request.Form)
form := &model.AdvancedStatusCreateForm{} form := &model.AdvancedStatusCreateForm{}
if err := c.ShouldBind(form); err != nil || form == nil { if err := c.ShouldBind(form); err != nil {
l.Debugf("could not parse form from request: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
return return
} }
l.Debugf("handling status request form: %+v", form)
// Give the fields on the request form a first pass to make sure the request is superficially valid.
l.Tracef("validating form %+v", form)
if err := validateCreateStatus(form); err != nil { if err := validateCreateStatus(form); err != nil {
l.Debugf("error validating form: %s", err) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
apiStatus, err := m.processor.StatusCreate(c.Request.Context(), authed, form) apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status create: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return return
} }
@ -120,7 +105,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
} }
func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
// validate that, structurally, we have a valid status/post
if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
return errors.New("no status, media, or poll provided") return errors.New("no status, media, or poll provided")
} }
@ -135,19 +119,16 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
maxPollChars := config.GetStatusesPollOptionMaxChars() maxPollChars := config.GetStatusesPollOptionMaxChars()
maxCwChars := config.GetStatusesCWMaxChars() maxCwChars := config.GetStatusesCWMaxChars()
// validate status
if form.Status != "" { if form.Status != "" {
if len(form.Status) > maxChars { if len(form.Status) > maxChars {
return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars) return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars)
} }
} }
// validate media attachments
if len(form.MediaIDs) > maxMediaFiles { if len(form.MediaIDs) > maxMediaFiles {
return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles) return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles)
} }
// validate poll
if form.Poll != nil { if form.Poll != nil {
if form.Poll.Options == nil { if form.Poll.Options == nil {
return errors.New("poll with no options") return errors.New("poll with no options")
@ -162,14 +143,12 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
} }
} }
// validate spoiler text/cw
if form.SpoilerText != "" { if form.SpoilerText != "" {
if len(form.SpoilerText) > maxCwChars { if len(form.SpoilerText) > maxCwChars {
return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars) return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars)
} }
} }
// validate post language
if form.Language != "" { if form.Language != "" {
if err := validate.Language(form.Language); err != nil { if err := validate.Language(form.Language); err != nil {
return err return err

View file

@ -256,7 +256,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
suite.Equal(`{"error":"bad request"}`, string(b)) suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
} }
// Post a reply to the status of a local user that allows replies. // Post a reply to the status of a local user that allows replies.

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -65,43 +66,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusDELETEHandler(c *gin.Context) { func (m *Module) StatusDELETEHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "StatusDELETEHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debug("not authed so can't delete status") api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiStatus, err := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status delete: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
// the status was already gone/never existed
if apiStatus == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
return return
} }

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -62,37 +63,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusFavePOSTHandler(c *gin.Context) { func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "StatusFavePOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debug("not authed so can't fave status") api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiStatus, err := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status fave: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return return
} }

View file

@ -118,13 +118,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
suite.statusModule.StatusFavePOSTHandler(ctx) suite.statusModule.StatusFavePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusForbidden, recorder.Code)
result := recorder.Result() result := recorder.Result()
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
assert.Equal(suite.T(), `{"error":"bad request"}`, string(b)) assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
} }
func TestStatusFaveTestSuite(t *testing.T) { func TestStatusFaveTestSuite(t *testing.T) {

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -63,37 +64,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusFavedByGETHandler(c *gin.Context) { func (m *Module) StatusFavedByGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ authed, err := oauth.Authed(c, true, true, true, true)
"func": "statusGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
if err != nil { if err != nil {
l.Errorf("error authing status faved by request: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiAccounts, err := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status faved by request: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return return
} }

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -54,45 +55,40 @@
// description: "The requested created status." // description: "The requested created status."
// schema: // schema:
// "$ref": "#/definitions/status" // "$ref": "#/definitions/status"
// '401':
// description: unauthorized
// '400': // '400':
// description: bad request // description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500': // '500':
// description: internal error // description: internal server error
func (m *Module) StatusGETHandler(c *gin.Context) { func (m *Module) StatusGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ authed, err := oauth.Authed(c, true, true, true, true)
"func": "statusGETHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, false, false, false, false)
if err != nil { if err != nil {
l.Errorf("error authing status faved by request: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiStatus, err := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status get: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return return
} }

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -63,37 +64,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "StatusUnboostPOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debug("not authed so can't unboost status") api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID) apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error processing status unboost: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -19,11 +19,12 @@
package status package status
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -62,37 +63,32 @@
// description: forbidden // description: forbidden
// '404': // '404':
// description: not found // description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "StatusUnfavePOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debug("not authed so can't unfave status") api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
targetStatusID := c.Param(IDKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) err := errors.New("no status id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
apiStatus, err := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
if err != nil { if errWithCode != nil {
l.Debugf("error processing status unfave: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return return
} }

View file

@ -2,14 +2,24 @@
import ( import (
"fmt" "fmt"
"github.com/sirupsen/logrus"
"net/http" "net/http"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// we expect cors requests (via eg., pinafore.social) so be lenient
CheckOrigin: func(r *http.Request) bool { return true },
}
// StreamGETHandler swagger:operation GET /api/v1/streaming streamGet // StreamGETHandler swagger:operation GET /api/v1/streaming streamGet
// //
// Initiate a websocket connection for live streaming of statuses and notifications. // Initiate a websocket connection for live streaming of statuses and notifications.
@ -108,79 +118,78 @@
// '400': // '400':
// description: bad request // description: bad request
func (m *Module) StreamGETHandler(c *gin.Context) { func (m *Module) StreamGETHandler(c *gin.Context) {
l := logrus.WithField("func", "StreamGETHandler")
streamType := c.Query(StreamQueryKey) streamType := c.Query(StreamQueryKey)
if streamType == "" { if streamType == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("no stream type provided under query key %s", StreamQueryKey)}) err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey)
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
accessToken := c.Query(AccessTokenQueryKey) accessToken := c.Query(AccessTokenQueryKey)
if accessToken == "" { if accessToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("no access token provided under query key %s", AccessTokenQueryKey)}) err := fmt.Errorf("no access token provided under query key %s", AccessTokenQueryKey)
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return return
} }
// make sure a valid token has been provided and obtain the associated account account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
account, err := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "could not authorize with given token"})
return
}
// prepare to upgrade the connection to a websocket connection
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// we fully expect cors requests (via something like pinafore.social) so we should be lenient here
return true
},
}
// do the actual upgrade here
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
l.Infof("error upgrading websocket connection: %s", err)
return
}
defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection
// inform the processor that we have a new connection and want a s for it
s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
if errWithCode != nil { if errWithCode != nil {
c.JSON(errWithCode.Code(), errWithCode.Safe()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return return
} }
defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler
// spawn a new ticker for pinging the connection periodically stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
t := time.NewTicker(30 * time.Second) if errWithCode != nil {
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
// we want to stay in the sendloop as long as possible while the client is connected -- the only thing that should break the loop is if the client leaves or something else goes wrong l := logrus.WithFields(logrus.Fields{
sendLoop: "account": account.Username,
"path": BasePath,
"streamID": stream.ID,
"streamType": streamType,
})
wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
// If the upgrade fails, then Upgrade replies to the client with an HTTP error response.
// Because websocket issues are a pretty common source of headaches, we should also log
// this at Error to make this plenty visible and help admins out a bit.
l.Errorf("error upgrading websocket connection: %s", err)
close(stream.Hangup)
return
}
defer func() {
// cleanup
wsConn.Close()
close(stream.Hangup)
}()
streamTicker := time.NewTicker(30 * time.Second)
// We want to stay in the loop as long as possible while the client is connected.
// The only thing that should break the loop is if the client leaves or the connection becomes unhealthy.
//
// If the loop does break, we expect the client to reattempt connection, so it's cheap to leave + try again
wsLoop:
for { for {
select { select {
case m := <-s.Messages: case m := <-stream.Messages:
// we've got a streaming message!!
l.Trace("received message from stream") l.Trace("received message from stream")
if err := conn.WriteJSON(m); err != nil { if err := wsConn.WriteJSON(m); err != nil {
l.Debugf("error writing json to websocket connection: %s", err) l.Debugf("error writing json to websocket connection; breaking off: %s", err)
// if something is wrong we want to bail and drop the connection -- the client will create a new one break wsLoop
break sendLoop
} }
l.Trace("wrote message into websocket connection") l.Trace("wrote message into websocket connection")
case <-t.C: case <-streamTicker.C:
l.Trace("received TICK from ticker") l.Trace("received TICK from ticker")
if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { if err := wsConn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
l.Debugf("error writing ping to websocket connection: %s", err) l.Debugf("error writing ping to websocket connection; breaking off: %s", err)
// if something is wrong we want to bail and drop the connection -- the client will create a new one break wsLoop
break sendLoop
} }
l.Trace("wrote ping message into websocket connection") l.Trace("wrote ping message into websocket connection")
} }
} }
l.Trace("leaving StreamGETHandler")
} }

View file

@ -19,13 +19,13 @@
package timeline package timeline
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -105,17 +105,14 @@
// '400': // '400':
// description: bad request // description: bad request
func (m *Module) HomeTimelineGETHandler(c *gin.Context) { func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
l := logrus.WithField("func", "HomeTimelineGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -142,8 +139,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -154,8 +151,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
if localString != "" { if localString != "" {
i, err := strconv.ParseBool(localString) i, err := strconv.ParseBool(localString)
if err != nil { if err != nil {
l.Debugf("error parsing local string: %s", err) err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
local = i local = i
@ -163,13 +160,12 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error from processor HomeTimelineGet: %s", errWithCode) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
if resp.LinkHeader != "" { if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader) c.Header("Link", resp.LinkHeader)
} }
c.JSON(http.StatusOK, resp.Statuses) c.JSON(http.StatusOK, resp.Items)
} }

View file

@ -19,13 +19,13 @@
package timeline package timeline
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -105,17 +105,14 @@
// '400': // '400':
// description: bad request // description: bad request
func (m *Module) PublicTimelineGETHandler(c *gin.Context) { func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
l := logrus.WithField("func", "PublicTimelineGETHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
@ -142,8 +139,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
if limitString != "" { if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64) i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil { if err != nil {
l.Debugf("error parsing limit string: %s", err) err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
limit = int(i) limit = int(i)
@ -154,8 +151,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
if localString != "" { if localString != "" {
i, err := strconv.ParseBool(localString) i, err := strconv.ParseBool(localString)
if err != nil { if err != nil {
l.Debugf("error parsing local string: %s", err) err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"}) api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
local = i local = i
@ -163,13 +160,12 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error from processor PublicTimelineGet: %s", errWithCode) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
if resp.LinkHeader != "" { if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader) c.Header("Link", resp.LinkHeader)
} }
c.JSON(http.StatusOK, resp.Statuses) c.JSON(http.StatusOK, resp.Items)
} }

View file

@ -19,12 +19,13 @@
package user package user
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -54,48 +55,48 @@
// responses: // responses:
// '200': // '200':
// description: Change successful // description: Change successful
// '400':
// description: bad request
// '401': // '401':
// description: unauthorized // description: unauthorized
// '403': // '403':
// description: forbidden // description: forbidden
// '400': // '406':
// description: bad request // description: not acceptable
// '500': // '500':
// description: "internal error" // description: internal error
func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { func (m *Module) PasswordChangePOSTHandler(c *gin.Context) {
l := logrus.WithField("func", "PasswordChangePOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true) authed, err := oauth.Authed(c, true, true, true, true)
if err != nil { if err != nil {
l.Debugf("error authing: %s", err) api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return return
} }
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
// First check this user/account is active.
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
l.Debugf("couldn't auth: %s", err)
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
return return
} }
form := &model.PasswordChangeRequest{} form := &model.PasswordChangeRequest{}
if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" { if err := c.ShouldBind(form); err != nil {
if err != nil { api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
l.Debugf("could not parse form from request: %s", err) return
} }
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
if form.OldPassword == "" {
err := errors.New("password change request missing field old_password")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if form.NewPassword == "" {
err := errors.New("password change request missing field new_password")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil { if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil {
l.Debugf("error changing user password: %s", errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View file

@ -49,7 +49,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json") ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"old_password": {"password"}, "old_password": {"password"},
@ -83,7 +83,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json") ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"new_password": {"peepeepoopoopassword"}, "new_password": {"peepeepoopoopassword"},
@ -97,7 +97,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
suite.Equal(`{"error":"missing one or more required form values"}`, string(b)) suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b))
} }
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
@ -110,7 +110,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json") ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"old_password": {"notright"}, "old_password": {"notright"},
@ -125,7 +125,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
suite.Equal(`{"error":"bad request: old password did not match"}`, string(b)) suite.Equal(`{"error":"Bad Request: old password did not match"}`, string(b))
} }
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
@ -138,7 +138,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json") ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"old_password": {"password"}, "old_password": {"password"},
@ -153,7 +153,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
suite.Equal(`{"error":"bad request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) suite.Equal(`{"error":"Bad Request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
} }
func TestPasswordChangeTestSuite(t *testing.T) { func TestPasswordChangeTestSuite(t *testing.T) {

View file

@ -56,8 +56,8 @@ type UserStandardTestSuite struct {
} }
func (suite *UserStandardTestSuite) SetupTest() { func (suite *UserStandardTestSuite) SetupTest() {
testrig.InitTestLog()
testrig.InitTestConfig() testrig.InitTestConfig()
testrig.InitTestLog()
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()

View file

@ -0,0 +1,154 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package api
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// TODO: add more templated html pages here for different error types
// NotFoundHandler serves a 404 html page through the provided gin context,
// if accept is 'text/html', or just returns a json error if 'accept' is empty
// or application/json.
//
// When serving html, NotFoundHandler calls the provided InstanceGet function
// to fetch the apimodel representation of the instance, for serving in the
// 404 header and footer.
//
// If an error is returned by InstanceGet, the function will panic.
func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) {
switch accept {
case string(TextHTML):
host := config.GetHost()
instance, err := instanceGet(c.Request.Context(), host)
if err != nil {
panic(err)
}
c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
"instance": instance,
})
default:
c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)})
}
}
// genericErrorHandler is a more general version of the NotFoundHandler, which can
// be used for serving either generic error pages with some rendered help text,
// or just some error json if the caller prefers (or has no preference).
func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) {
switch accept {
case string(TextHTML):
host := config.GetHost()
instance, err := instanceGet(c.Request.Context(), host)
if err != nil {
panic(err)
}
c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
"instance": instance,
"code": errWithCode.Code(),
"error": errWithCode.Safe(),
})
default:
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
}
}
// ErrorHandler takes the provided gin context and errWithCode and tries to serve
// a helpful error to the caller. It will do content negotiation to figure out if
// the caller prefers to see an html page with the error rendered there. If not, or
// if something goes wrong during the function, it will recover and just try to serve
// an appropriate application/json content-type error.
func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) {
path := c.Request.URL.Path
if raw := c.Request.URL.RawQuery; raw != "" {
path = path + "?" + raw
}
l := logrus.WithFields(logrus.Fields{
"path": path,
"error": errWithCode.Error(),
})
statusCode := errWithCode.Code()
if statusCode == http.StatusInternalServerError {
l.Error("Internal Server Error")
} else {
l.Debug("handling error")
}
// if we panic for any reason during error handling,
// we should still try to return a basic code
defer func() {
if p := recover(); p != nil {
l.Warnf("recovered from panic: %s", p)
c.JSON(statusCode, gin.H{"error": errWithCode.Safe()})
}
}()
// discover if we're allowed to serve a nice html error page,
// or if we should just use a json. Normally we would want to
// check for a returned error, but if an error occurs here we
// can just fall back to default behavior (serve json error).
accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...)
if statusCode == http.StatusNotFound {
// use our special not found handler with useful status text
NotFoundHandler(c, instanceGet, accept)
} else {
genericErrorHandler(c, instanceGet, accept, errWithCode)
}
}
// OAuthErrorHandler is a lot like ErrorHandler, but it specifically returns errors
// that are compatible with https://datatracker.ietf.org/doc/html/rfc6749#section-5.2,
// but serializing errWithCode.Error() in the 'error' field, and putting any help text
// from the error in the 'error_description' field. This means you should be careful not
// to pass any detailed errors (that might contain sensitive information) into the
// errWithCode.Error() field, since the client will see this. Use your noggin!
func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) {
l := logrus.WithFields(logrus.Fields{
"path": c.Request.URL.Path,
"error": errWithCode.Error(),
"help": errWithCode.Safe(),
})
statusCode := errWithCode.Code()
if statusCode == http.StatusInternalServerError {
l.Error("Internal Server Error")
} else {
l.Debug("handling OAuth error")
}
c.JSON(statusCode, gin.H{
"error": errWithCode.Error(),
"error_description": errWithCode.Safe(),
})
}

34
internal/api/mime.go Normal file
View file

@ -0,0 +1,34 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package api
// MIME represents a mime-type.
type MIME string
// MIME type
const (
AppJSON MIME = `application/json`
AppXML MIME = `application/xml`
AppActivityJSON MIME = `application/activity+json`
AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
AppForm MIME = `application/x-www-form-urlencoded`
MultipartForm MIME = `multipart/form-data`
TextXML MIME = `text/xml`
TextHTML MIME = `text/html`
)

View file

@ -43,3 +43,24 @@ type Notification struct {
// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. // Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.
Status *Status `json:"status,omitempty"` Status *Status `json:"status,omitempty"`
} }
/*
The below functions are added onto the apimodel notification so that it satisfies
the Timelineable interface in internal/timeline.
*/
func (n *Notification) GetID() string {
return n.ID
}
func (n *Notification) GetAccountID() string {
return ""
}
func (n *Notification) GetBoostOfID() string {
return ""
}
func (n *Notification) GetBoostOfAccountID() string {
return ""
}

View file

@ -18,9 +18,11 @@
package model package model
// StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link import "github.com/superseriousbusiness/gotosocial/internal/timeline"
// TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link
// header for the previous and next queries, to be returned to the client. // header for the previous and next queries, to be returned to the client.
type StatusTimelineResponse struct { type TimelineResponse struct {
Statuses []*Status Items []timeline.Timelineable
LinkHeader string LinkHeader string
} }

View file

@ -25,33 +25,40 @@
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Offer represents an offered mime-type.
type Offer string
const (
AppJSON Offer = `application/json` // AppJSON is the mime type for 'application/json'.
AppActivityJSON Offer = `application/activity+json` // AppActivityJSON is the mime type for 'application/activity+json'.
AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
TextHTML Offer = `text/html` // TextHTML is the mime type for 'text/html'.
)
// ActivityPubAcceptHeaders represents the Accept headers mentioned here: // ActivityPubAcceptHeaders represents the Accept headers mentioned here:
// https://www.w3.org/TR/activitypub/#retrieving-objects //
var ActivityPubAcceptHeaders = []Offer{ var ActivityPubAcceptHeaders = []MIME{
AppActivityJSON, AppActivityJSON,
AppActivityLDJSON, AppActivityLDJSON,
} }
// JSONAcceptHeaders is a slice of offers that just contains application/json types. // JSONAcceptHeaders is a slice of offers that just contains application/json types.
var JSONAcceptHeaders = []Offer{ var JSONAcceptHeaders = []MIME{
AppJSON,
}
// HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will
// fall back to JSON if necessary. This is useful for error handling, since it can
// be used to serve a nice HTML page if the caller accepts that, or just JSON if not.
var HTMLOrJSONAcceptHeaders = []MIME{
TextHTML,
AppJSON, AppJSON,
} }
// HTMLAcceptHeaders is a slice of offers that just contains text/html types. // HTMLAcceptHeaders is a slice of offers that just contains text/html types.
var HTMLAcceptHeaders = []Offer{ var HTMLAcceptHeaders = []MIME{
TextHTML, TextHTML,
} }
// HTMLOrActivityPubHeaders matches text/html first, then activitypub types.
// This is useful for user URLs that a user might go to in their browser.
// https://www.w3.org/TR/activitypub/#retrieving-objects
var HTMLOrActivityPubHeaders = []MIME{
TextHTML,
AppActivityJSON,
AppActivityLDJSON,
}
// NegotiateAccept takes the *gin.Context from an incoming request, and a // NegotiateAccept takes the *gin.Context from an incoming request, and a
// slice of Offers, and performs content negotiation for the given request // slice of Offers, and performs content negotiation for the given request
// with the given content-type offers. It will return a string representation // with the given content-type offers. It will return a string representation
@ -73,7 +80,7 @@
// often-used Accept types. // often-used Accept types.
// //
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation
func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) { func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) {
if len(offers) == 0 { if len(offers) == 0 {
return "", errors.New("no format offered") return "", errors.New("no format offered")
} }

View file

@ -23,8 +23,8 @@
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
) )
// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet // NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet
@ -45,27 +45,22 @@
// schema: // schema:
// "$ref": "#/definitions/nodeinfo" // "$ref": "#/definitions/nodeinfo"
func (m *Module) NodeInfoGETHandler(c *gin.Context) { func (m *Module) NodeInfoGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "NodeInfoGETHandler",
"user-agent": c.Request.UserAgent(),
})
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request) ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
if errWithCode != nil {
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
b, err := json.Marshal(ni)
if err != nil { if err != nil {
l.Debugf("error with get node info request: %s", err) api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
c.JSON(err.Code(), err.Safe())
return return
} }
b, jsonErr := json.Marshal(ni)
if jsonErr != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()})
}
c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b) c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b)
} }

View file

@ -22,8 +22,8 @@
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
) )
// NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet // NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet
@ -45,19 +45,14 @@
// schema: // schema:
// "$ref": "#/definitions/wellKnownResponse" // "$ref": "#/definitions/wellKnownResponse"
func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{
"func": "NodeInfoWellKnownGETHandler",
})
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
if err != nil { if errWithCode != nil {
l.Debugf("error with get node info rel request: %s", err) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(err.Code(), err.Safe())
return return
} }

View file

@ -20,48 +20,45 @@
import ( import (
"encoding/json" "encoding/json"
"fmt" "errors"
"net/http" "net/http"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
) )
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. // FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
func (m *Module) FollowersGETHandler(c *gin.Context) { func (m *Module) FollowersGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ // usernames on our instance are always lowercase
"func": "FollowersGETHandler", requestedUsername := strings.ToLower(c.Param(UsernameKey))
"url": c.Request.RequestURI,
})
requestedUsername := c.Param(UsernameKey)
if requestedUsername == "" { if requestedUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) err := errors.New("no username specified in request")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil { if err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
l.Tracef("negotiated format: %s", format)
ctx := transferContext(c) if format == string(api.TextHTML) {
// redirect to the user's profile
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
}
followers, errWithCode := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL) resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL)
if errWithCode != nil { if errWithCode != nil {
l.Info(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
b, mErr := json.Marshal(followers) b, err := json.Marshal(resp)
if mErr != nil { if err != nil {
err := fmt.Errorf("could not marshal json: %s", mErr) api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
l.Error(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }

View file

@ -20,48 +20,45 @@
import ( import (
"encoding/json" "encoding/json"
"fmt" "errors"
"net/http" "net/http"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
) )
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. // FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
func (m *Module) FollowingGETHandler(c *gin.Context) { func (m *Module) FollowingGETHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ // usernames on our instance are always lowercase
"func": "FollowingGETHandler", requestedUsername := strings.ToLower(c.Param(UsernameKey))
"url": c.Request.RequestURI,
})
requestedUsername := c.Param(UsernameKey)
if requestedUsername == "" { if requestedUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) err := errors.New("no username specified in request")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil { if err != nil {
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return return
} }
l.Tracef("negotiated format: %s", format)
ctx := transferContext(c) if format == string(api.TextHTML) {
// redirect to the user's profile
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
}
following, errWithCode := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL) resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL)
if errWithCode != nil { if errWithCode != nil {
l.Info(errWithCode.Error()) api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }
b, mErr := json.Marshal(following) b, err := json.Marshal(resp)
if mErr != nil { if err != nil {
err := fmt.Errorf("could not marshal json: %s", mErr) api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
l.Error(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }

View file

@ -19,43 +19,33 @@
package user package user
import ( import (
"net/http" "errors"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck "github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
) )
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox. // InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
// Eg., POST to https://example.org/users/whatever/inbox. // Eg., POST to https://example.org/users/whatever/inbox.
func (m *Module) InboxPOSTHandler(c *gin.Context) { func (m *Module) InboxPOSTHandler(c *gin.Context) {
l := logrus.WithFields(logrus.Fields{ // usernames on our instance are always lowercase
"func": "InboxPOSTHandler", requestedUsername := strings.ToLower(c.Param(UsernameKey))
"url": c.Request.RequestURI,
})
requestedUsername := c.Param(UsernameKey)
if requestedUsername == "" { if requestedUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) err := errors.New("no username specified in request")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return return
} }
ctx := transferContext(c) if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil {
posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request)
if err != nil {
if withCode, ok := err.(gtserror.WithCode); ok { if withCode, ok := err.(gtserror.WithCode); ok {
l.Debugf("InboxPOSTHandler: %s", withCode.Error()) api.ErrorHandler(c, withCode, m.processor.InstanceGet)
c.JSON(withCode.Code(), withCode.Safe()) } else {
return api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
} }
l.Debugf("InboxPOSTHandler: error processing request: %s", err) } else if !posted {
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) err := errors.New("unable to process request")
return api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
}
if !posted {
l.Debugf("InboxPOSTHandler: request could not be handled as an AP request; headers were: %+v", c.Request.Header)
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
} }
} }

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