Files
racket-upnp/resource-capabilities.rkt
T

201 lines
6.6 KiB
Racket

#lang racket/base
;; Resolve DLNA resource capabilities advertised by an HTTP media server.
(require net/http-client
net/url
racket/match
racket/port
racket/string
simple-log)
(provide resolve-dlna-resource-protocol-info
clear-dlna-resource-capability-cache!)
(sl-def-log upnp-resource)
(define capability-cache (make-hash))
(define capability-cache-lock (make-semaphore 1))
(define (clear-dlna-resource-capability-cache!)
(call-with-semaphore
capability-cache-lock
(lambda () (hash-clear! capability-cache))))
(define (url-request-target value)
(let* ([relative
(struct-copy url value
[scheme #f]
[user #f]
[host #f]
[port #f]
[fragment #f])]
[target (url->string relative)])
(if (string=? target "") "/" target)))
(define (header-value headers name)
(for/or ([header (in-list headers)])
(let* ([text
(if (bytes? header)
(bytes->string/latin-1 header)
header)]
[match
(and (string? text)
(regexp-match
#px"^([^:]+):[ \t]*(.*?)[ \t]*$"
text))])
(and match
(string-ci=? (cadr match) name)
(caddr match)))))
(define (protocol-info-parts protocol-info)
(and protocol-info
(regexp-match #px"^([^:]*):([^:]*):([^:]*):(.*)$"
protocol-info)))
(define (protocol-info-additional-info protocol-info)
(let ([parts (protocol-info-parts protocol-info)])
(and parts (list-ref parts 4))))
(define (replace-additional-info protocol-info additional-info)
(match (protocol-info-parts protocol-info)
[(list _ protocol network content-type _)
(string-join
(list protocol network content-type additional-info)
":")]
[_ protocol-info]))
(define (call-with-timeout seconds thunk)
(let ([custodian (make-custodian)]
[result-channel (make-channel)])
(parameterize ([current-custodian custodian])
(thread
(lambda ()
(with-handlers
([exn:fail?
(lambda (exception)
(channel-put result-channel
(cons #f exception)))])
(channel-put result-channel
(cons #t (thunk)))))))
(let ([result (sync/timeout seconds result-channel)])
(custodian-shutdown-all custodian)
(cond
[(not result)
(error 'resolve-dlna-resource-protocol-info
"HTTP capability request timed out")]
[(car result) (cdr result)]
[else (raise (cdr result))]))))
(define (request-content-features uri timeout)
(let* ([url-value (string->url uri)]
[scheme (or (url-scheme url-value) "http")]
[ssl? (string-ci=? scheme "https")]
[host (url-host url-value)]
[port (or (url-port url-value) (if ssl? 443 80))])
(unless (and host
(or (string-ci=? scheme "http")
(string-ci=? scheme "https")))
(raise-arguments-error
'resolve-dlna-resource-protocol-info
"resource is not an HTTP URI"
"uri" uri))
(call-with-timeout
timeout
(lambda ()
(let-values ([(status headers in)
(http-sendrecv
host
(url-request-target url-value)
#:ssl? ssl?
#:port port
#:method #"HEAD"
#:headers
(list "getcontentFeatures.dlna.org: 1"
"transferMode.dlna.org: Streaming"
"Connection: close"))])
(void status)
(close-input-port in)
(header-value headers "contentFeatures.dlna.org"))))))
(define (base-protocol-info protocol-info mime-type)
(cond
[(and (string? protocol-info)
(not (string=? (string-trim protocol-info) "")))
protocol-info]
[(and (string? mime-type)
(not (string=? (string-trim mime-type) "")))
(format "http-get:*:~a:*" mime-type)]
[else "http-get:*:application/octet-stream:*"]))
(define (resolve-dlna-resource-protocol-info
uri
#:protocol-info [protocol-info #f]
#:mime-type [mime-type #f]
#:timeout [timeout 5.0])
(unless (and (string? uri)
(not (string=? (string-trim uri) "")))
(raise-argument-error
'resolve-dlna-resource-protocol-info
"non-empty-string?"
uri))
(for ([value (in-list (list protocol-info mime-type))])
(unless (or (not value) (string? value))
(raise-argument-error
'resolve-dlna-resource-protocol-info
"(or/c #f string?)"
value)))
(unless (and (rational? timeout) (positive? timeout))
(raise-argument-error
'resolve-dlna-resource-protocol-info
"positive-real?"
timeout))
(let* ([base (base-protocol-info protocol-info mime-type)]
[additional-info (protocol-info-additional-info base)])
(cond
[(and additional-info
(not (string=? additional-info "*")))
base]
[else
(let* ([key (list uri base)]
[cached
(call-with-semaphore
capability-cache-lock
(lambda () (hash-ref capability-cache key #f)))])
(or cached
(with-handlers
([exn:fail?
(lambda (exception)
(warn-upnp-resource
"Could not resolve DLNA capabilities for ~a: ~a"
uri
(exn-message exception))
base)])
(let ([content-features
(request-content-features uri timeout)])
(if (and content-features
(not (string=? content-features "")))
(let ([resolved
(replace-additional-info
base
content-features)])
(dbg-upnp-resource
"Resolved DLNA protocolInfo for ~a: ~a"
uri
resolved)
(call-with-semaphore
capability-cache-lock
(lambda ()
(hash-set! capability-cache key resolved)))
resolved)
base)))))])))
(module+ test
(require rackunit)
(check-equal?
(replace-additional-info
"http-get:*:audio/flac:*"
"DLNA.ORG_OP=01;DLNA.ORG_CI=0")
"http-get:*:audio/flac:DLNA.ORG_OP=01;DLNA.ORG_CI=0"))