11package auth
22
33import (
4+ "context"
45 "encoding/json"
56 "fmt"
7+ "io"
68 "net/http"
79 "net/url"
810 "path"
911 "strings"
12+ "sync"
13+ "time"
1014
1115 "github.com/sirupsen/logrus"
1216)
@@ -21,6 +25,10 @@ type derivedResourceRequest struct {
2125 Sources []derivedSource `json:"sources"`
2226}
2327
28+ var profileProbeClient = & http.Client {Timeout : 2 * time .Second }
29+
30+ const solidOIDCIssuerIRI = "http://www.w3.org/ns/solid/terms#oidcIssuer"
31+
2432func HandleDerivedResourceRequest (w http.ResponseWriter , r * http.Request ) {
2533 if r .Method != http .MethodPost {
2634 http .Error (w , "Method not allowed" , http .StatusMethodNotAllowed )
@@ -179,6 +187,12 @@ func deriveOwnerWebID(sourceURL string) string {
179187 return ""
180188 }
181189
190+ candidates := candidateOwnerWebIDs (parsed )
191+ if resolved := resolveOwnerFromCandidates (candidates ); resolved != "" {
192+ return resolved
193+ }
194+
195+ // Fallback heuristic when remote probing is unavailable.
182196 if strings .Contains (parsed .Path , "/profile/card" ) {
183197 base := strings .TrimSuffix (parsed .Path , "#me" )
184198 if ! strings .HasSuffix (base , "/card" ) && ! strings .HasSuffix (base , "/profile/card" ) {
@@ -194,3 +208,117 @@ func deriveOwnerWebID(sourceURL string) string {
194208
195209 return fmt .Sprintf ("%s://%s/%s/profile/card#me" , parsed .Scheme , parsed .Host , segments [0 ])
196210}
211+
212+ func candidateOwnerWebIDs (parsed * url.URL ) []string {
213+ base := fmt .Sprintf ("%s://%s" , parsed .Scheme , parsed .Host )
214+ candidates := []string {
215+ base + "/profile/card#me" ,
216+ }
217+
218+ segments := strings .Split (strings .Trim (parsed .Path , "/" ), "/" )
219+ if len (segments ) == 1 && segments [0 ] == "" {
220+ return uniqueStringList (candidates )
221+ }
222+
223+ // Treat the last segment as a file path component when it looks file-like.
224+ if ! strings .HasSuffix (parsed .Path , "/" ) && len (segments ) > 0 && strings .Contains (segments [len (segments )- 1 ], "." ) {
225+ segments = segments [:len (segments )- 1 ]
226+ }
227+
228+ for i := 1 ; i <= len (segments ); i ++ {
229+ prefix := strings .Join (segments [:i ], "/" )
230+ candidates = append (candidates , fmt .Sprintf ("%s/%s/profile/card#me" , base , prefix ))
231+ }
232+
233+ return uniqueStringList (candidates )
234+ }
235+
236+ func resolveOwnerFromCandidates (candidates []string ) string {
237+ if len (candidates ) == 0 {
238+ return ""
239+ }
240+
241+ type probeResult struct {
242+ index int
243+ ok bool
244+ }
245+
246+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
247+ defer cancel ()
248+
249+ results := make (chan probeResult , len (candidates ))
250+ var wg sync.WaitGroup
251+ for i , candidate := range candidates {
252+ wg .Add (1 )
253+ go func (idx int , webID string ) {
254+ defer wg .Done ()
255+ results <- probeResult {index : idx , ok : probeProfileCard (ctx , webID )}
256+ }(i , candidate )
257+ }
258+
259+ wg .Wait ()
260+ close (results )
261+
262+ // Prefer the most specific successful candidate (highest index).
263+ bestIndex := - 1
264+ for result := range results {
265+ if result .ok && result .index > bestIndex {
266+ bestIndex = result .index
267+ }
268+ }
269+ if bestIndex < 0 {
270+ return ""
271+ }
272+ return candidates [bestIndex ]
273+ }
274+
275+ func probeProfileCard (ctx context.Context , webID string ) bool {
276+ parsed , err := url .Parse (strings .TrimSpace (webID ))
277+ if err != nil || parsed .Scheme == "" || parsed .Host == "" {
278+ return false
279+ }
280+ parsed .Fragment = ""
281+
282+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , parsed .String (), nil )
283+ if err != nil {
284+ return false
285+ }
286+ req .Header .Set ("Accept" , "text/turtle,application/ld+json,application/json,*/*" )
287+
288+ resp , err := profileProbeClient .Do (req )
289+ if err != nil {
290+ return false
291+ }
292+ defer resp .Body .Close ()
293+ body , err := io .ReadAll (io .LimitReader (resp .Body , 1 << 20 ))
294+ if err != nil {
295+ return false
296+ }
297+
298+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
299+ return false
300+ }
301+ return hasOIDCIssuerPredicate (body )
302+ }
303+
304+ func hasOIDCIssuerPredicate (body []byte ) bool {
305+ if len (body ) == 0 {
306+ return false
307+ }
308+ payload := strings .ToLower (string (body ))
309+ return strings .Contains (payload , strings .ToLower (solidOIDCIssuerIRI )) ||
310+ strings .Contains (payload , "solid:oidcissuer" )
311+ }
312+
313+ func uniqueStringList (values []string ) []string {
314+ seen := make (map [string ]struct {}, len (values ))
315+ out := make ([]string , 0 , len (values ))
316+ for _ , value := range values {
317+ if _ , ok := seen [value ]; ok {
318+ continue
319+ }
320+ seen [value ] = struct {}{}
321+ out = append (out , value )
322+ }
323+ return out
324+ }
0 commit comments