Return 401s when logged out of more activitypub api endpoints (#750)
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -19,6 +20,8 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
var ErrProfilePrivate = errors.New("profile is private")
|
||||
|
||||
type WebfingerResponse struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []struct {
|
||||
@@ -220,7 +223,7 @@ func assembleActor(actor *core.Record, dbActor *core.Record, app core.App, inclu
|
||||
}
|
||||
|
||||
if private {
|
||||
return dbActor, fmt.Errorf("profile is private")
|
||||
return dbActor, ErrProfilePrivate
|
||||
}
|
||||
|
||||
return dbActor, nil
|
||||
@@ -361,7 +364,7 @@ func FetchCollection(actor *core.Record, url string) (*pub.OrderedCollection, er
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("profile is private")
|
||||
return nil, ErrProfilePrivate
|
||||
}
|
||||
return nil, fmt.Errorf("collection fetch %s returned: %v", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -1166,7 +1167,7 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) {
|
||||
}
|
||||
return err
|
||||
} else if err != nil && actor != nil {
|
||||
if err.Error() == "profile is private" {
|
||||
if errors.Is(err, federation.ErrProfilePrivate) {
|
||||
// this is our own profile
|
||||
if e.Auth != nil && actor.GetString("user") == e.Auth.Id {
|
||||
return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": nil})
|
||||
@@ -1214,7 +1215,7 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) {
|
||||
}
|
||||
collection, err := federation.FetchCollection(userActor, fmt.Sprintf("%s?page=%d", url, intPage))
|
||||
if err != nil {
|
||||
if err.Error() == "profile is private" {
|
||||
if errors.Is(err, federation.ErrProfilePrivate) {
|
||||
return e.JSON(http.StatusNotFound, map[string]any{"error": "profile is private"})
|
||||
}
|
||||
return err
|
||||
|
||||
35
web/src/lib/util/activitypub_server_util.ts
Normal file
35
web/src/lib/util/activitypub_server_util.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
import { ClientResponseError } from 'pocketbase';
|
||||
|
||||
import type { Actor } from '$lib/models/activitypub/actor';
|
||||
import { isRemoteHandle } from '$lib/util/activitypub_util';
|
||||
|
||||
type GetActorOptions = {
|
||||
follows?: boolean;
|
||||
};
|
||||
|
||||
type ActorResponse = {
|
||||
actor: Actor;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export async function getActorResponseForHandle(event: RequestEvent, handle: string, options: GetActorOptions = {}): Promise<ActorResponse> {
|
||||
const origin = env.ORIGIN!;
|
||||
const remoteHandle = isRemoteHandle(handle, origin);
|
||||
if (remoteHandle && !event.locals.user) {
|
||||
throw new ClientResponseError({ status: 401, response: { message: "Unauthorized" } });
|
||||
}
|
||||
const searchParams = new URLSearchParams();
|
||||
if (options.follows) {
|
||||
searchParams.set("follows", "true");
|
||||
}
|
||||
const query = searchParams.toString();
|
||||
|
||||
const encodedHandle = encodeURIComponent(handle);
|
||||
const response: ActorResponse = await event.locals.pb.send(
|
||||
`/activitypub/actor?resource=acct:${encodedHandle}${query ? `&${query}` : ""}`,
|
||||
{ method: "GET", fetch: event.fetch },
|
||||
);
|
||||
return response;
|
||||
}
|
||||
@@ -3,8 +3,13 @@
|
||||
export function splitUsername(handle: string, localDomain?: string) {
|
||||
const cleaned = handle.replace(/^@/, "").trim();
|
||||
|
||||
let normalizedLocalDomain = localDomain;
|
||||
if (normalizedLocalDomain && normalizedLocalDomain.includes("://")) {
|
||||
normalizedLocalDomain = new URL(normalizedLocalDomain).hostname;
|
||||
}
|
||||
|
||||
if (!cleaned.includes("@")) {
|
||||
return [cleaned, localDomain];
|
||||
return [cleaned, normalizedLocalDomain];
|
||||
}
|
||||
|
||||
let [user, domain] = cleaned.split("@");
|
||||
@@ -12,6 +17,22 @@ export function splitUsername(handle: string, localDomain?: string) {
|
||||
return [user, domain]
|
||||
}
|
||||
|
||||
export function isRemoteHandle(handle: string, origin: string) {
|
||||
const [, domain] = splitUsername(handle, origin);
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
let normalizedDomain = domain;
|
||||
try {
|
||||
normalizedDomain = new URL(`http://${domain}`).hostname;
|
||||
} catch {
|
||||
normalizedDomain = domain.split(":")[0];
|
||||
}
|
||||
normalizedDomain = normalizedDomain.replace(/^www\./, "");
|
||||
const localHost = new URL(origin).hostname.replace(/^www\./, "");
|
||||
return normalizedDomain.toLowerCase() !== localHost.toLowerCase();
|
||||
}
|
||||
|
||||
export function handleFromRecordWithIRI(record: any) {
|
||||
if (!record.expand?.author) {
|
||||
throw new Error("object has no author info")
|
||||
@@ -23,4 +44,4 @@ export function handleFromRecordWithIRI(record: any) {
|
||||
const url = new URL(record.iri ?? "")
|
||||
|
||||
return `@${record.expand.author.preferred_username}@${url.hostname}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Actor } from '$lib/models/activitypub/actor';
|
||||
import { FollowCreateSchema } from '$lib/models/api/follow_schema';
|
||||
import type { Follow } from '$lib/models/follow';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { APIError, Collection, handleError, list } from '$lib/util/api_util';
|
||||
import { json, type RequestEvent } from '@sveltejs/kit';
|
||||
import type { APOrderedCollectionPage } from 'activitypub-types';
|
||||
@@ -19,7 +20,7 @@ export async function GET(event: RequestEvent) {
|
||||
throw new APIError(400, "invalid params")
|
||||
}
|
||||
|
||||
const { actor }: { actor: Actor } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
const { actor } = await getActorResponseForHandle(event, handle);
|
||||
|
||||
const page = event.url.searchParams.get("page") ?? "1"
|
||||
|
||||
@@ -81,4 +82,4 @@ export async function PUT(event: RequestEvent) {
|
||||
} catch (e) {
|
||||
return handleError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { Profile } from '$lib/models/profile';
|
||||
import { splitUsername } from '$lib/util/activitypub_util';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
@@ -10,12 +9,8 @@ export async function GET(event: RequestEvent) {
|
||||
return error(400, { message: "Bad request" })
|
||||
}
|
||||
|
||||
if(splitUsername(handle)[1] !== undefined && !event.locals.user) {
|
||||
return error(401, { message: "Unauthorized" })
|
||||
}
|
||||
|
||||
try {
|
||||
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}&follows=true`, { method: "GET", fetch: event.fetch, });
|
||||
const { actor, error: actorError } = await getActorResponseForHandle(event, handle, { follows: true });
|
||||
|
||||
const profile: Profile = {
|
||||
id: actor.id!,
|
||||
@@ -28,7 +23,7 @@ export async function GET(event: RequestEvent) {
|
||||
followers: actor.followerCount ?? 0,
|
||||
following: actor.followingCount ?? 0,
|
||||
icon: actor.icon ?? "",
|
||||
error
|
||||
error: actorError ?? undefined
|
||||
}
|
||||
|
||||
return json({ profile, actor: actor })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
|
||||
import { type FeedItem } from '$lib/models/feed';
|
||||
import type { Trail } from '$lib/models/trail';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { Collection, handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import { ClientResponseError, type ListResult } from 'pocketbase';
|
||||
@@ -12,7 +13,7 @@ export async function GET(event: RequestEvent) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
const { actor } = await getActorResponseForHandle(event, handle);
|
||||
|
||||
const searchParams = Object.fromEntries(event.url.searchParams);
|
||||
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TrailSearchResult } from '$lib/models/trail';
|
||||
import type { ListSearchResult } from '$lib/stores/search_store';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import type { SearchResponse } from 'meilisearch';
|
||||
@@ -12,7 +13,7 @@ export async function POST(event: RequestEvent) {
|
||||
}
|
||||
|
||||
try {
|
||||
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
const { actor } = await getActorResponseForHandle(event, handle);
|
||||
|
||||
const data = await event.request.json()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
|
||||
import type { SummitLog } from '$lib/models/summit_log';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { Collection, handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import { ClientResponseError, type ListResult } from 'pocketbase';
|
||||
@@ -9,9 +10,9 @@ export async function GET(event: RequestEvent) {
|
||||
if (!handle) {
|
||||
return error(400, { message: "Bad request" })
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
const { actor } = await getActorResponseForHandle(event, handle);
|
||||
|
||||
const searchParams = Object.fromEntries(event.url.searchParams);
|
||||
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
||||
@@ -20,7 +21,7 @@ export async function GET(event: RequestEvent) {
|
||||
safeSearchParams.filter = safeSearchParams.filter + `&&author='${actor.id}'`
|
||||
}else {
|
||||
safeSearchParams.filter = `author='${actor.id}'`
|
||||
}
|
||||
}
|
||||
|
||||
let summitLogs: SummitLog[];
|
||||
if (actor.isLocal) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TrailSearchResult } from '$lib/models/trail';
|
||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||
import { handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import type { SearchResponse } from 'meilisearch';
|
||||
@@ -11,7 +12,7 @@ export async function POST(event: RequestEvent) {
|
||||
}
|
||||
|
||||
try {
|
||||
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
const { actor } = await getActorResponseForHandle(event, handle);
|
||||
|
||||
const data = await event.request.json()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user