From 433443ffa9f41dbf548d1c63d3355221c6a63e9b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 18 Jun 2022 22:06:32 +0800 Subject: [PATCH 01/16] Dump should only copy regular files and symlink regular files (#20015) --- cmd/dump.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/dump.go b/cmd/dump.go index ea41c0c029d9b..d807cb0587c13 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -22,7 +22,7 @@ import ( "code.gitea.io/gitea/modules/util" "gitea.com/go-chi/session" - archiver "github.com/mholt/archiver/v3" + "github.com/mholt/archiver/v3" "github.com/urfave/cli" ) @@ -439,8 +439,23 @@ func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeA } } } else { - if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil { - return err + // only copy regular files and symlink regular files, skip non-regular files like socket/pipe/... + shouldAdd := file.Mode().IsRegular() + if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink { + target, err := filepath.EvalSymlinks(currentAbsPath) + if err != nil { + return err + } + targetStat, err := os.Stat(target) + if err != nil { + return err + } + shouldAdd = targetStat.Mode().IsRegular() + } + if shouldAdd { + if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil { + return err + } } } } From 439ace607d23e91da4b39a389730ab8a01c4eb56 Mon Sep 17 00:00:00 2001 From: Wim Date: Sat, 18 Jun 2022 19:08:34 +0200 Subject: [PATCH 02/16] Return 404 when tag is broken (#20017) Fixes #19979 --- modules/context/repo.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/context/repo.go b/modules/context/repo.go index 8e75ad07d58f2..217cbd3dfca33 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -942,6 +942,10 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName) if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound("GetTagCommit", err) + return + } ctx.ServerError("GetTagCommit", err) return } From e4ceaf65fbf626a7ccb97c0ee5a0108ea143b584 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 19 Jun 2022 03:23:06 +0800 Subject: [PATCH 03/16] Update document to clarify that ALLOWED_DOMAINS/BLOCKED_DOMAINS support wildcard (#20016) --- custom/conf/app.example.ini | 2 ++ docs/content/doc/advanced/config-cheat-sheet.en-us.md | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 065c57ef51d5c..8e5bfeac8ebe0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2231,10 +2231,12 @@ PATH = ;; ;; Allowed domains for migrating, default is blank. Blank means everything will be allowed. ;; Multiple domains could be separated by commas. +;; Wildcard is supported: "github.com, *.github.com" ;ALLOWED_DOMAINS = ;; ;; Blocklist for migrating, default is blank. Multiple domains could be separated by commas. ;; When ALLOWED_DOMAINS is not blank, this option has a higher priority to deny domains. +;; Wildcard is supported. ;BLOCKED_DOMAINS = ;; ;; Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291 (false by default) diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 4f041d417edf8..bce45d4da0d62 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -1081,8 +1081,8 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations. - `RETRY_BACKOFF`: **3**: Backoff time per http/https request retry (seconds) -- `ALLOWED_DOMAINS`: **\**: Domains allowlist for migrating repositories, default is blank. It means everything will be allowed. Multiple domains could be separated by commas. -- `BLOCKED_DOMAINS`: **\**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option has a higher priority to deny domains. +- `ALLOWED_DOMAINS`: **\**: Domains allowlist for migrating repositories, default is blank. It means everything will be allowed. Multiple domains could be separated by commas. Wildcard is supported: `github.com, *.github.com`. +- `BLOCKED_DOMAINS`: **\**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option has a higher priority to deny domains. Wildcard is supported. - `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291 - `SKIP_TLS_VERIFY`: **false**: Allow skip tls verify From 909fb6ad20f2209e1c55d9c387614c42f001447a Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 19 Jun 2022 01:26:22 +0200 Subject: [PATCH 04/16] Backtick table name in generic orphan check (#20019) --- modules/doctor/dbconsistency.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/doctor/dbconsistency.go b/modules/doctor/dbconsistency.go index 349a2121cf572..f8b62e898a4bd 100644 --- a/modules/doctor/dbconsistency.go +++ b/modules/doctor/dbconsistency.go @@ -201,10 +201,10 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er "oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"), // find stopwatches without existing user genericOrphanCheck("Orphaned Stopwatches without existing User", - "stopwatch", "user", "stopwatch.user_id=user.id"), + "stopwatch", "user", "stopwatch.user_id=`user`.id"), // find stopwatches without existing issue genericOrphanCheck("Orphaned Stopwatches without existing Issue", - "stopwatch", "issue", "stopwatch.issue_id=issue.id"), + "stopwatch", "issue", "stopwatch.issue_id=`issue`.id"), ) for _, c := range consistencyChecks { From e86f18a05a19611c518f6ccb87cc7a1261392666 Mon Sep 17 00:00:00 2001 From: Anthony Wang Date: Sun, 19 Jun 2022 00:25:12 -0500 Subject: [PATCH 05/16] User keypairs and HTTP signatures for ActivityPub federation using go-ap (#19133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * go.mod: add go-fed/{httpsig,activity/pub,activity/streams} dependency go get github.com/go-fed/activity/streams@master go get github.com/go-fed/activity/pub@master go get github.com/go-fed/httpsig@master * activitypub: implement /api/v1/activitypub/user/{username} (#14186) Return informations regarding a Person (as defined in ActivityStreams https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person). Refs: https://github.com/go-gitea/gitea/issues/14186 Signed-off-by: Loïc Dachary * activitypub: add the public key to Person (#14186) Refs: https://github.com/go-gitea/gitea/issues/14186 Signed-off-by: Loïc Dachary * activitypub: go-fed conformant Clock instance Signed-off-by: Loïc Dachary * activitypub: signing http client Signed-off-by: Loïc Dachary * activitypub: implement the ReqSignature middleware Signed-off-by: Loïc Dachary * activitypub: hack_16834 Signed-off-by: Loïc Dachary * Fix CI checks-backend errors with go mod tidy Signed-off-by: Anthony Wang * Change 2021 to 2022, properly format package imports Signed-off-by: Anthony Wang * Run make fmt and make generate-swagger Signed-off-by: Anthony Wang * Use Gitea JSON library, add assert for pkp Signed-off-by: Anthony Wang * Run make fmt again, fix err var redeclaration Signed-off-by: Anthony Wang * Remove LogSQL from ActivityPub person test Signed-off-by: Anthony Wang * Assert if json.Unmarshal succeeds Signed-off-by: Anthony Wang * Cleanup, handle invalid usernames for ActivityPub person GET request Signed-off-by: Anthony Wang * Rename hack_16834 to user_settings Signed-off-by: Anthony Wang * Use the httplib module instead of http for GET requests * Clean up whitespace with make fmt * Use time.RFC1123 and make the http.Client proxy-aware * Check if digest algo is supported in setting module * Clean up some variable declarations * Remove unneeded copy * Use system timezone instead of setting.DefaultUILocation * Use named constant for httpsigExpirationTime * Make pubKey IRI #main-key instead of /#main-key * Move /#main-key to #main-key in tests * Implemented Webfinger endpoint. * Add visible check. * Add user profile as alias. * Add actor IRI and remote interaction URL to WebFinger response * fmt * Fix lint errors * Use go-ap instead of go-fed * Run go mod tidy to fix missing modules in go.mod and go.sum * make fmt * Convert remaining code to go-ap * Clean up go.sum * Fix JSON unmarshall error * Fix CI errors by adding @context to Person() and making sure types match * Correctly decode JSON in api_activitypub_person_test.go * Force CI rerun * Fix TestActivityPubPersonInbox segfault * Fix lint error * Use @mariusor's suggestions for idiomatic go-ap usage * Correctly add inbox/outbox IRIs to person * Code cleanup * Remove another LogSQL from ActivityPub person test * Move httpsig algos slice to an init() function * Add actor IRI and remote interaction URL to WebFinger response * Update TestWebFinger to check for ActivityPub IRI in aliases * make fmt * Force CI rerun * WebFinger: Add CORS header and fix Href -> Template for remote interactions The CORS header is needed due to https://datatracker.ietf.org/doc/html/rfc7033#section-5 and fixes some Peertube <-> Gitea federation issues * make lint-backend * Make sure Person endpoint has Content-Type application/activity+json and includes PreferredUsername, URL, and Icon Setting the correct Content-Type is essential for federating with Mastodon * Use UTC instead of GMT * Rename pkey to pubKey * Make sure HTTP request Date in GMT * make fmt * dont drop err * Make sure API responses always refer to username in original case Copied from what I wrote on #19133 discussion: Handling username case is a very tricky issue and I've already encountered a Mastodon <-> Gitea federation bug due to Gitea considering Ta180m and ta180m to be the same user while Mastodon thinks they are two different users. I think the best way forward is for Gitea to only use the original case version of the username for federation so other AP software don't get confused. * Move httpsig algs constant slice to modules/setting/federation.go * Add new federation settings to app.example.ini and config-cheat-sheet * Return if marshalling error * Make sure Person IRIs are generated correctly This commit ensures that if the setting.AppURL is something like "http://127.0.0.1:42567" (like in the integration tests), a trailing slash will be added after that URL. * If httpsig verification fails, fix Host header and try again This fixes a very rare bug when Gitea and another AP server (confirmed to happen with Mastodon) are running on the same machine, Gitea fails to verify incoming HTTP signatures. This is because the other AP server creates the sig with the public Gitea domain as the Host. However, when Gitea receives the request, the Host header is instead localhost, so the signature verification fails. Manually changing the host header to the correct value and trying the veification again fixes the bug. * Revert "If httpsig verification fails, fix Host header and try again" This reverts commit f53e46c721a037c55facb9200106a6b491bf834c. The bug was actually caused by nginx messing up the Host header when reverse-proxying since I didn't have the line `proxy_set_header Host $host;` in my nginx config for Gitea. * Go back to using ap.IRI to generate inbox and outbox IRIs * use const for key values * Update routers/web/webfinger.go * Use ctx.JSON in Person response to make code cleaner * Revert "Use ctx.JSON in Person response to make code cleaner" This doesn't work because the ctx.JSON() function already sends the response out and it's too late to edit the headers. This reverts commit 95aad988975be3393c76094864ed6ba962157e0c. * Use activitypub.ActivityStreamsContentType for Person response Content Type * Limit maximum ActivityPub request and response sizes to a configurable setting * Move setting key constants to models/user/setting_keys.go * Fix failing ActivityPubPerson integration test by checking the correct field for username * Add a warning about changing settings that can break federation * Add better comments * Don't multiply Federation.MaxSize by 1<<20 twice * Add more better comments * Fix failing ActivityPubMissingPerson test We now use ctx.ContextUser so the message printed out when a user does not exist is slightly different * make generate-swagger For some reason I didn't realize that /templates/swagger/v1_json.tmpl was machine-generated by make generate-swagger... I've been editing it by hand for three months! 🤦 * Move getting the RFC 2616 time to a separate function * More code cleanup * Update go-ap to fix empty liked collection and removed unneeded HTTP headers * go mod tidy * Add ed25519 to httpsig algorithms * Use go-ap/jsonld to add @context and marshal JSON * Change Gitea user agent from the default to Gitea/Version * Use ctx.ServerError and remove all remote interaction code from webfinger.go --- custom/conf/app.example.ini | 17 +++ .../doc/advanced/config-cheat-sheet.en-us.md | 8 ++ go.mod | 7 +- go.sum | 14 +- integrations/api_activitypub_person_test.go | 103 +++++++++++++++ integrations/webfinger_test.go | 2 +- models/user/setting_keys.go | 4 + modules/activitypub/client.go | 124 ++++++++++++++++++ modules/activitypub/client_test.go | 49 +++++++ modules/activitypub/main_test.go | 18 +++ modules/activitypub/user_settings.go | 45 +++++++ modules/activitypub/user_settings_test.go | 29 ++++ modules/setting/federation.go | 30 ++++- modules/structs/activitypub.go | 10 ++ routers/api/v1/activitypub/person.go | 106 +++++++++++++++ routers/api/v1/activitypub/reqsignature.go | 102 ++++++++++++++ routers/api/v1/api.go | 7 + routers/api/v1/swagger/activitypub.go | 16 +++ routers/web/webfinger.go | 7 + templates/swagger/v1_json.tmpl | 69 ++++++++++ 20 files changed, 762 insertions(+), 5 deletions(-) create mode 100644 integrations/api_activitypub_person_test.go create mode 100644 modules/activitypub/client.go create mode 100644 modules/activitypub/client_test.go create mode 100644 modules/activitypub/main_test.go create mode 100644 modules/activitypub/user_settings.go create mode 100644 modules/activitypub/user_settings_test.go create mode 100644 modules/structs/activitypub.go create mode 100644 routers/api/v1/activitypub/person.go create mode 100644 routers/api/v1/activitypub/reqsignature.go create mode 100644 routers/api/v1/swagger/activitypub.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 8e5bfeac8ebe0..c4b8913e41b8d 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2253,6 +2253,23 @@ PATH = ;; ;; Enable/Disable user statistics for nodeinfo if federation is enabled ; SHARE_USER_STATISTICS = true +;; +;; Maximum federation request and response size (MB) +; MAX_SIZE = 4 +;; +;; WARNING: Changing the settings below can break federation. +;; +;; HTTP signature algorithms +; ALGORITHMS = rsa-sha256, rsa-sha512, ed25519 +;; +;; HTTP signature digest algorithm +; DIGEST_ALGORITHM = SHA-256 +;; +;; GET headers for federation requests +; GET_HEADERS = (request-target), Date +;; +;; POST headers for federation requests +; POST_HEADERS = (request-target), Date, Digest ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index bce45d4da0d62..b50e630a83824 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -1090,6 +1090,14 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `ENABLED`: **true**: Enable/Disable federation capabilities - `SHARE_USER_STATISTICS`: **true**: Enable/Disable user statistics for nodeinfo if federation is enabled +- `MAX_SIZE`: **4**: Maximum federation request and response size (MB) + + WARNING: Changing the settings below can break federation. + +- `ALGORITHMS`: **rsa-sha256, rsa-sha512, ed25519**: HTTP signature algorithms +- `DIGEST_ALGORITHM`: **SHA-256**: HTTP signature digest algorithm +- `GET_HEADERS`: **(request-target), Date**: GET headers for federation requests +- `POST_HEADERS`: **(request-target), Date, Digest**: POST headers for federation requests ## Packages (`packages`) diff --git a/go.mod b/go.mod index e06ccfe13d84f..e9b4194c79936 100644 --- a/go.mod +++ b/go.mod @@ -28,10 +28,12 @@ require ( github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.2 github.com/gliderlabs/ssh v0.3.4 + github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b + github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-enry/go-enry/v2 v2.8.2 - github.com/go-fed/httpsig v1.1.0 + github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-git/go-billy/v5 v5.3.1 github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 github.com/go-ldap/ldap/v3 v3.4.3 @@ -107,6 +109,7 @@ require ( require ( cloud.google.com/go v0.99.0 // indirect + git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2 // indirect github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 // indirect @@ -160,6 +163,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fullstorydev/grpcurl v1.8.1 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect @@ -252,6 +256,7 @@ require ( github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/unknwon/com v1.0.1 // indirect + github.com/valyala/fastjson v1.6.3 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect diff --git a/go.sum b/go.sum index 9c99adfbe14cc..ae4d06d1f9916 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EU contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2 h1:2OrsyJYZp7J6nyAsKi2q1SELYRaIc0aQmcQ/EQqPfk8= +git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb h1:Yy0Bxzc8R2wxiwXoG/rECGplJUSpXqCsog9PuJFgiHs= gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= @@ -460,6 +462,12 @@ github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b h1:+RjYfEfoZdM3wHFs752dlOpGaoRhwRRyQxjajg08LcQ= +github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b/go.mod h1:DE3vvc6Didgfd3k7M1Mos6qMDFNmMrxJmYVMHG9h9Io= +github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f h1:kJhGo4NApJP0Lt9lkJnfmuTnRWVFbCynY0kiTxpPUR4= +github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f/go.mod h1:KHkKFKZvc05lr79+RGoq/zG8YjWi3+FK60Bxd+mpCew= +github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d h1:Z/oRXMlZHjvjIqDma1FrIGL3iE5YL7MUI0bwYEZ6qbA= +github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= @@ -472,8 +480,8 @@ github.com/go-enry/go-enry/v2 v2.8.2 h1:uiGmC+3K8sVd/6DOe2AOJEOihJdqda83nPyJNtMR github.com/go-enry/go-enry/v2 v2.8.2/go.mod h1:GVzIiAytiS5uT/QiuakK7TF1u4xDab87Y8V5EJRpsIQ= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= -github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= -github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= +github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= @@ -1507,6 +1515,8 @@ github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw= github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= +github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= diff --git a/integrations/api_activitypub_person_test.go b/integrations/api_activitypub_person_test.go new file mode 100644 index 0000000000000..4898d5e01d8ef --- /dev/null +++ b/integrations/api_activitypub_person_test.go @@ -0,0 +1,103 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/activitypub" + "code.gitea.io/gitea/modules/setting" + + ap "github.com/go-ap/activitypub" + "github.com/stretchr/testify/assert" +) + +func TestActivityPubPerson(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + setting.Federation.Enabled = true + defer func() { + setting.Federation.Enabled = false + }() + + username := "user2" + req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username)) + resp := MakeRequest(t, req, http.StatusOK) + body := resp.Body.Bytes() + assert.Contains(t, string(body), "@context") + + var person ap.Person + err := person.UnmarshalJSON(body) + assert.NoError(t, err) + + assert.Equal(t, ap.PersonType, person.Type) + assert.Equal(t, username, person.PreferredUsername.String()) + keyID := person.GetID().String() + assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID) + assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.Outbox.GetID().String()) + assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.Inbox.GetID().String()) + + pubKey := person.PublicKey + assert.NotNil(t, pubKey) + publicKeyID := keyID + "#main-key" + assert.Equal(t, pubKey.ID.String(), publicKeyID) + + pubKeyPem := pubKey.PublicKeyPem + assert.NotNil(t, pubKeyPem) + assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem) + }) +} + +func TestActivityPubMissingPerson(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + setting.Federation.Enabled = true + defer func() { + setting.Federation.Enabled = false + }() + + req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser") + resp := MakeRequest(t, req, http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "user redirect does not exist") + }) +} + +func TestActivityPubPersonInbox(t *testing.T) { + srv := httptest.NewServer(c) + defer srv.Close() + + onGiteaRun(t, func(*testing.T, *url.URL) { + appURL := setting.AppURL + setting.Federation.Enabled = true + setting.AppURL = srv.URL + defer func() { + setting.Federation.Enabled = false + setting.Database.LogSQL = false + setting.AppURL = appURL + }() + username1 := "user1" + ctx := context.Background() + user1, err := user_model.GetUserByName(ctx, username1) + assert.NoError(t, err) + user1url := fmt.Sprintf("%s/api/v1/activitypub/user/%s#main-key", srv.URL, username1) + c, err := activitypub.NewClient(user1, user1url) + assert.NoError(t, err) + username2 := "user2" + user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user/%s/inbox", srv.URL, username2) + + // Signed request succeeds + resp, err := c.Post([]byte{}, user2inboxurl) + assert.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + // Unsigned request fails + req := NewRequest(t, "POST", user2inboxurl) + MakeRequest(t, req, http.StatusInternalServerError) + }) +} diff --git a/integrations/webfinger_test.go b/integrations/webfinger_test.go index 8ba93c3f204b8..07bf58b509fe7 100644 --- a/integrations/webfinger_test.go +++ b/integrations/webfinger_test.go @@ -52,7 +52,7 @@ func TestWebfinger(t *testing.T) { var jrd webfingerJRD DecodeJSON(t, resp, &jrd) assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject) - assert.ElementsMatch(t, []string{user.HTMLURL()}, jrd.Aliases) + assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(user.Name)}, jrd.Aliases) req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, "unknown.host")) MakeRequest(t, req, http.StatusBadRequest) diff --git a/models/user/setting_keys.go b/models/user/setting_keys.go index 109b5dd916365..d48ac930527a6 100644 --- a/models/user/setting_keys.go +++ b/models/user/setting_keys.go @@ -9,4 +9,8 @@ const ( SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types" // SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour" + // UserActivityPubPrivPem is user's private key + UserActivityPubPrivPem = "activitypub.priv_pem" + // UserActivityPubPubPem is user's public key + UserActivityPubPubPem = "activitypub.pub_pem" ) diff --git a/modules/activitypub/client.go b/modules/activitypub/client.go new file mode 100644 index 0000000000000..738b1e47372bb --- /dev/null +++ b/modules/activitypub/client.go @@ -0,0 +1,124 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "net/http" + "strings" + "time" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/proxy" + "code.gitea.io/gitea/modules/setting" + + "github.com/go-fed/httpsig" +) + +const ( + // ActivityStreamsContentType const + ActivityStreamsContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` + httpsigExpirationTime = 60 +) + +// Gets the current time as an RFC 2616 formatted string +// RFC 2616 requires RFC 1123 dates but with GMT instead of UTC +func CurrentTime() string { + return strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT") +} + +func containsRequiredHTTPHeaders(method string, headers []string) error { + var hasRequestTarget, hasDate, hasDigest bool + for _, header := range headers { + hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget + hasDate = hasDate || header == "Date" + hasDigest = hasDigest || header == "Digest" + } + if !hasRequestTarget { + return fmt.Errorf("missing http header for %s: %s", method, httpsig.RequestTarget) + } else if !hasDate { + return fmt.Errorf("missing http header for %s: Date", method) + } else if !hasDigest && method != http.MethodGet { + return fmt.Errorf("missing http header for %s: Digest", method) + } + return nil +} + +// Client struct +type Client struct { + client *http.Client + algs []httpsig.Algorithm + digestAlg httpsig.DigestAlgorithm + getHeaders []string + postHeaders []string + priv *rsa.PrivateKey + pubID string +} + +// NewClient function +func NewClient(user *user_model.User, pubID string) (c *Client, err error) { + if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { + return + } else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { + return + } + + priv, err := GetPrivateKey(user) + if err != nil { + return + } + privPem, _ := pem.Decode([]byte(priv)) + privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) + if err != nil { + return + } + + c = &Client{ + client: &http.Client{ + Transport: &http.Transport{ + Proxy: proxy.Proxy(), + }, + }, + algs: setting.HttpsigAlgs, + digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm), + getHeaders: setting.Federation.GetHeaders, + postHeaders: setting.Federation.PostHeaders, + priv: privParsed, + pubID: pubID, + } + return +} + +// NewRequest function +func (c *Client) NewRequest(b []byte, to string) (req *http.Request, err error) { + buf := bytes.NewBuffer(b) + req, err = http.NewRequest(http.MethodPost, to, buf) + if err != nil { + return + } + req.Header.Add("Content-Type", ActivityStreamsContentType) + req.Header.Add("Date", CurrentTime()) + req.Header.Add("User-Agent", "Gitea/"+setting.AppVer) + signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime) + if err != nil { + return + } + err = signer.SignRequest(c.priv, c.pubID, req, b) + return +} + +// Post function +func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) { + var req *http.Request + if req, err = c.NewRequest(b, to); err != nil { + return + } + resp, err = c.client.Do(req) + return +} diff --git a/modules/activitypub/client_test.go b/modules/activitypub/client_test.go new file mode 100644 index 0000000000000..b93ef5ac988ab --- /dev/null +++ b/modules/activitypub/client_test.go @@ -0,0 +1,49 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + + _ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 + + "github.com/stretchr/testify/assert" +) + +func TestActivityPubSignedPost(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) + pubID := "https://example.com/pubID" + c, err := NewClient(user, pubID) + assert.NoError(t, err) + + expected := "BODY" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest")) + assert.Contains(t, r.Header.Get("Signature"), pubID) + assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, expected, string(body)) + fmt.Fprintf(w, expected) + })) + defer srv.Close() + + r, err := c.Post([]byte(expected), srv.URL) + assert.NoError(t, err) + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, expected, string(body)) +} diff --git a/modules/activitypub/main_test.go b/modules/activitypub/main_test.go new file mode 100644 index 0000000000000..7fa2b09265cec --- /dev/null +++ b/modules/activitypub/main_test.go @@ -0,0 +1,18 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", ".."), + }) +} diff --git a/modules/activitypub/user_settings.go b/modules/activitypub/user_settings.go new file mode 100644 index 0000000000000..2144e7b47fb3b --- /dev/null +++ b/modules/activitypub/user_settings.go @@ -0,0 +1,45 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + user_model "code.gitea.io/gitea/models/user" +) + +// GetKeyPair function returns a user's private and public keys +func GetKeyPair(user *user_model.User) (pub, priv string, err error) { + var settings map[string]*user_model.Setting + settings, err = user_model.GetUserSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem}) + if err != nil { + return + } else if len(settings) == 0 { + if priv, pub, err = GenerateKeyPair(); err != nil { + return + } + if err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, priv); err != nil { + return + } + if err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPubPem, pub); err != nil { + return + } + return + } else { + priv = settings[user_model.UserActivityPubPrivPem].SettingValue + pub = settings[user_model.UserActivityPubPubPem].SettingValue + return + } +} + +// GetPublicKey function returns a user's public key +func GetPublicKey(user *user_model.User) (pub string, err error) { + pub, _, err = GetKeyPair(user) + return +} + +// GetPrivateKey function returns a user's private key +func GetPrivateKey(user *user_model.User) (priv string, err error) { + _, priv, err = GetKeyPair(user) + return +} diff --git a/modules/activitypub/user_settings_test.go b/modules/activitypub/user_settings_test.go new file mode 100644 index 0000000000000..90c6f680f993e --- /dev/null +++ b/modules/activitypub/user_settings_test.go @@ -0,0 +1,29 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + _ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 + + "github.com/stretchr/testify/assert" +) + +func TestUserSettings(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) + pub, priv, err := GetKeyPair(user1) + assert.NoError(t, err) + pub1, err := GetPublicKey(user1) + assert.NoError(t, err) + assert.Equal(t, pub, pub1) + priv1, err := GetPrivateKey(user1) + assert.NoError(t, err) + assert.Equal(t, priv, priv1) +} diff --git a/modules/setting/federation.go b/modules/setting/federation.go index fd39e5c7c2aff..b06d0a9219097 100644 --- a/modules/setting/federation.go +++ b/modules/setting/federation.go @@ -4,21 +4,49 @@ package setting -import "code.gitea.io/gitea/modules/log" +import ( + "code.gitea.io/gitea/modules/log" + + "github.com/go-fed/httpsig" +) // Federation settings var ( Federation = struct { Enabled bool ShareUserStatistics bool + MaxSize int64 + Algorithms []string + DigestAlgorithm string + GetHeaders []string + PostHeaders []string }{ Enabled: true, ShareUserStatistics: true, + MaxSize: 4, + Algorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"}, + DigestAlgorithm: "SHA-256", + GetHeaders: []string{"(request-target)", "Date"}, + PostHeaders: []string{"(request-target)", "Date", "Digest"}, } ) +// Constant slice of httpsig algorithm objects +var HttpsigAlgs []httpsig.Algorithm + func newFederationService() { if err := Cfg.Section("federation").MapTo(&Federation); err != nil { log.Fatal("Failed to map Federation settings: %v", err) + } else if !httpsig.IsSupportedDigestAlgorithm(Federation.DigestAlgorithm) { + log.Fatal("unsupported digest algorithm: %s", Federation.DigestAlgorithm) + return + } + + // Get MaxSize in bytes instead of MiB + Federation.MaxSize = 1 << 20 * Federation.MaxSize + + HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.Algorithms)) + for i, alg := range Federation.Algorithms { + HttpsigAlgs[i] = httpsig.Algorithm(alg) } } diff --git a/modules/structs/activitypub.go b/modules/structs/activitypub.go new file mode 100644 index 0000000000000..86681bf9d75aa --- /dev/null +++ b/modules/structs/activitypub.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package structs + +// ActivityPub type +type ActivityPub struct { + Context string `json:"@context"` +} diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go new file mode 100644 index 0000000000000..7290f1cbd9842 --- /dev/null +++ b/routers/api/v1/activitypub/person.go @@ -0,0 +1,106 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/activitypub" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + ap "github.com/go-ap/activitypub" + "github.com/go-ap/jsonld" +) + +// Person function returns the Person actor for a user +func Person(ctx *context.APIContext) { + // swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson + // --- + // summary: Returns the Person actor for a user + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActivityPub" + + link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name + person := ap.PersonNew(ap.IRI(link)) + + person.Name = ap.NaturalLanguageValuesNew() + err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName)) + if err != nil { + ctx.ServerError("Set Name", err) + return + } + + person.PreferredUsername = ap.NaturalLanguageValuesNew() + err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name)) + if err != nil { + ctx.ServerError("Set PreferredUsername", err) + return + } + + person.URL = ap.IRI(ctx.ContextUser.HTMLURL()) + + person.Icon = ap.Image{ + Type: ap.ImageType, + MediaType: "image/png", + URL: ap.IRI(ctx.ContextUser.AvatarLink()), + } + + person.Inbox = ap.IRI(link + "/inbox") + person.Outbox = ap.IRI(link + "/outbox") + + person.PublicKey.ID = ap.IRI(link + "#main-key") + person.PublicKey.Owner = ap.IRI(link) + + publicKeyPem, err := activitypub.GetPublicKey(ctx.ContextUser) + if err != nil { + ctx.ServerError("GetPublicKey", err) + return + } + person.PublicKey.PublicKeyPem = publicKeyPem + + binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person) + if err != nil { + ctx.ServerError("MarshalJSON", err) + return + } + ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType) + ctx.Resp.WriteHeader(http.StatusOK) + if _, err = ctx.Resp.Write(binary); err != nil { + log.Error("write to resp err: %v", err) + } +} + +// PersonInbox function handles the incoming data for a user inbox +func PersonInbox(ctx *context.APIContext) { + // swagger:operation POST /activitypub/user/{username}/inbox activitypub activitypubPersonInbox + // --- + // summary: Send to the inbox + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // responses: + // "204": + // "$ref": "#/responses/empty" + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go new file mode 100644 index 0000000000000..b870d1c0f9ee4 --- /dev/null +++ b/routers/api/v1/activitypub/reqsignature.go @@ -0,0 +1,102 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package activitypub + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "net/http" + "net/url" + + "code.gitea.io/gitea/modules/activitypub" + gitea_context "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/setting" + + ap "github.com/go-ap/activitypub" + "github.com/go-fed/httpsig" +) + +func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) { + person := ap.PersonNew(ap.IRI(keyID.String())) + err = person.UnmarshalJSON(b) + if err != nil { + err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %v", err) + return + } + pubKey := person.PublicKey + if pubKey.ID.String() != keyID.String() { + err = fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b)) + return + } + pubKeyPem := pubKey.PublicKeyPem + block, _ := pem.Decode([]byte(pubKeyPem)) + if block == nil || block.Type != "PUBLIC KEY" { + err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") + return + } + p, err = x509.ParsePKIXPublicKey(block.Bytes) + return +} + +func fetch(iri *url.URL) (b []byte, err error) { + req := httplib.NewRequest(iri.String(), http.MethodGet) + req.Header("Accept", activitypub.ActivityStreamsContentType) + req.Header("User-Agent", "Gitea/"+setting.AppVer) + resp, err := req.Response() + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status) + return + } + b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize)) + return +} + +func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) { + r := ctx.Req + + // 1. Figure out what key we need to verify + v, err := httpsig.NewVerifier(r) + if err != nil { + return + } + ID := v.KeyId() + idIRI, err := url.Parse(ID) + if err != nil { + return + } + // 2. Fetch the public key of the other actor + b, err := fetch(idIRI) + if err != nil { + return + } + pubKey, err := getPublicKeyFromResponse(b, idIRI) + if err != nil { + return + } + // 3. Verify the other actor's key + algo := httpsig.Algorithm(setting.Federation.Algorithms[0]) + authenticated = v.Verify(pubKey, algo) == nil + return +} + +// ReqHTTPSignature function +func ReqHTTPSignature() func(ctx *gitea_context.APIContext) { + return func(ctx *gitea_context.APIContext) { + if authenticated, err := verifyHTTPSignatures(ctx); err != nil { + ctx.ServerError("verifyHttpSignatures", err) + } else if !authenticated { + ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed") + } + } +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 03f7a57d5c861..c93606ae88308 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -81,6 +81,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/activitypub" "code.gitea.io/gitea/routers/api/v1/admin" "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/routers/api/v1/notify" @@ -643,6 +644,12 @@ func Routes() *web.Route { m.Get("/version", misc.Version) if setting.Federation.Enabled { m.Get("/nodeinfo", misc.NodeInfo) + m.Group("/activitypub", func() { + m.Group("/user/{username}", func() { + m.Get("", activitypub.Person) + m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) + }, context_service.UserAssignmentAPI()) + }) } m.Get("/signing-key.gpg", misc.SigningKey) m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) diff --git a/routers/api/v1/swagger/activitypub.go b/routers/api/v1/swagger/activitypub.go new file mode 100644 index 0000000000000..afc0c05057d60 --- /dev/null +++ b/routers/api/v1/swagger/activitypub.go @@ -0,0 +1,16 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package swagger + +import ( + api "code.gitea.io/gitea/modules/structs" +) + +// ActivityPub +// swagger:response ActivityPub +type swaggerResponseActivityPub struct { + // in:body + Body api.ActivityPub `json:"body"` +} diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index 8402967867e59..c4808fbfd2419 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -86,6 +86,7 @@ func WebfingerQuery(ctx *context.Context) { aliases := []string{ u.HTMLURL(), + appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name), } if !u.KeepEmailPrivate { aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email)) @@ -101,8 +102,14 @@ func WebfingerQuery(ctx *context.Context) { Rel: "http://webfinger.net/rel/avatar", Href: u.AvatarLink(), }, + { + Rel: "self", + Type: "application/activity+json", + Href: appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name), + }, } + ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*") ctx.JSON(http.StatusOK, &webfingerJRD{ Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host), Aliases: aliases, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4da8b12af4bb1..f3f9a33672253 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -23,6 +23,58 @@ }, "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", "paths": { + "/activitypub/user/{username}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "activitypub" + ], + "summary": "Returns the Person actor for a user", + "operationId": "activitypubPerson", + "parameters": [ + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActivityPub" + } + } + } + }, + "/activitypub/user/{username}/inbox": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "activitypub" + ], + "summary": "Send to the inbox", + "operationId": "activitypubPersonInbox", + "parameters": [ + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + } + } + } + }, "/admin/cron": { "get": { "produces": [ @@ -13113,6 +13165,17 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActivityPub": { + "description": "ActivityPub type", + "type": "object", + "properties": { + "@context": { + "type": "string", + "x-go-name": "Context" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "AddCollaboratorOption": { "description": "AddCollaboratorOption options when adding a user as a collaborator of a repository", "type": "object", @@ -18794,6 +18857,12 @@ } } }, + "ActivityPub": { + "description": "ActivityPub", + "schema": { + "$ref": "#/definitions/ActivityPub" + } + }, "AnnotatedTag": { "description": "AnnotatedTag", "schema": { From cc42c6488a818499e363935c214d5c4d0aaff554 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sun, 19 Jun 2022 18:05:15 +0800 Subject: [PATCH 06/16] fix delete pull head ref for DeleteIssue (#20032) * fix delete pull head ref for DeleteIssue fix #19655 Signed-off-by: a1012112796 <1012112796@qq.com> * add different help message for delete pull request Signed-off-by: a1012112796 <1012112796@qq.com> Co-authored-by: 6543 <6543@obermui.de> --- options/locale/locale_en-US.ini | 3 +++ routers/web/repo/issue.go | 5 +++++ services/issue/issue.go | 2 +- templates/repo/issue/view_content/sidebar.tmpl | 12 ++++++++++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 347022fbdb39b..72253b3640866 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1610,6 +1610,9 @@ pulls.auto_merge_canceled_schedule = The auto merge was canceled for this pull r pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto merge when all checks succeed %[1]s` pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s` +pulls.delete.title = Delete this pull request? +pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) + milestones.new = New Milestone milestones.closed = Closed %s milestones.update_ago = Updated %s ago diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 11d2daeeff0da..5b72ff79af370 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -878,6 +878,11 @@ func DeleteIssue(ctx *context.Context) { return } + if issue.IsPull { + ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther) + return + } + ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther) } diff --git a/services/issue/issue.go b/services/issue/issue.go index ded281e209e2b..467bc14b84594 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -149,7 +149,7 @@ func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *issues_m // delete pull request related git data if issue.IsPull { - if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d", git.PullPrefix, issue.PullRequest.Index)); err != nil { + if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d/head", git.PullPrefix, issue.PullRequest.Index)); err != nil { return err } } diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 7a966ce8788f4..d58e6e7b9ff4d 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -660,11 +660,19 @@