Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support adding custom headers to server connection/requests #254

Open
rmkjr opened this issue Jun 24, 2022 · 44 comments
Open

Support adding custom headers to server connection/requests #254

rmkjr opened this issue Jun 24, 2022 · 44 comments
Labels
enhancement New feature or request

Comments

@rmkjr
Copy link

rmkjr commented Jun 24, 2022

Is your feature request related to a problem? Please describe.

Zero Trust like app access, in particular Cloudflare Tunnels, are becoming quite popular in the self-hosted world. They allow you to make your self-hosted applications available externally without opening ports. However, they typically use a OpenID Connect or SAML style SSO approach to authenticate access which means an intermediate login page appears before connectivity to the application endpoint is then allowed. As a workaround to client apps that cannot support this additional authentication step, Cloudflare supports the use of service tokens passed with requests as additional headers.
https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access

Describe the solution you'd like

Support adding additional custom headers, custom keys and values, when setting up the server connection to allow us to set additional headers for requests made to the server. A great app citizen that has done this quite well is LunaSea.
https://docs.lunasea.app/modules/sonarr#custom-headers
https://imgur.com/a/JSC1bYn

Describe alternatives you've considered

The only alternative would be to forward the ports directly from the internet, or to always have a VPN or similar running when remote. Allowing custom headers to be set for the server connection would allow for very secure remote access.

Additional context

N/A

@rmkjr rmkjr added the enhancement New feature or request label Jun 24, 2022
@advplyr
Copy link
Owner

advplyr commented Jun 24, 2022

Thanks for the detailed request with an example implementation. Do you think this is something that would be implemented in the server settings and used for all clients? Or is this something on the connect screen of the mobile app where you would specify custom headers?

@rmkjr
Copy link
Author

rmkjr commented Jun 24, 2022

Typically I would see this implemented on the connect screen of the mobile app. If you were to say have multiple unique users, you may want to give them unique headers to authenticate to the tunnel broker in addition to their normal Audiobookshelf username/password.

That should also be a bit simpler. As the mobile app, if a user provides some number of extra headers via optional fields, would just need to append them to the http requests. The server and its authentication mechanisms could just ignore them as it's only the tunnel broker that is using them.

@advplyr
Copy link
Owner

advplyr commented Jun 24, 2022

The way adding a new server connection config works now is you first enter your server address

image

On submit the mobile app will ping the address using https://abs.example.com/ping

Would this /ping request require custom headers already added?

@rmkjr
Copy link
Author

rmkjr commented Jun 25, 2022

That is my understanding yes. Every http request made to the server endpoint should append on any added custom headers.

In the case of a Cloudflare tunnel, the url entered into the app is a Cloudflare endpoint. When a request is made to the endpoint the first thing it does is check the request headers. If the service auth token headers exist, or if a valid cookie header from a past interactive authentication session exist, then Cloudflare will pass that traffic to the Audiobookshelf server uninterrupted. If no such header(s) exist, then Cloudflare will attempt to redirect that request to the configured authentication IdP for authentication. By creating a service auth token and appending it to all request, Cloudflare will see that request as already authenticated, and pass it to the underlying endpoint, in this case the Audiobookshelf server, without needing the app to handle any extra authentication pages/steps.

So to your point, you would have to give users the opportunity to add/set their custom headers at that onboarding screen so they exist and can be added to the first request made to the server, as well as every following request.

LunaSea does it as a secondary screen. Basically a button on that submit button screen to add custom headers. That button then opens an additional screen where a user can add an arbitrary number of custom key/value pairs. Then you go back to the screen with the server url, and when a user clicks submit the added keys and values will be sent as appended request headers for all future requests including the first one to /ping.

@advplyr
Copy link
Owner

advplyr commented Jun 25, 2022

Got it. I'm not sure what the best UI/UX is for this given I did this a bit differently then most servers with the /ping step.
We could re-design this or add a "Add Custom Headers" button to the left of the submit button which would open a modal to put in custom headers.

@rmkjr
Copy link
Author

rmkjr commented Jun 25, 2022

The idea of an extra button to open a modal like you've described I think makes total sense.

I'm on both your TestFlight and Google Play beta, and also have Cloudflare tunnels setup for other apps, so I'll definitely be able to help you test it out.

Have to say btw, this whole thing is super cool. Have been playing around with ABS after getting my instance stood up this week and it's quite amazing! Thanks for thinking up and creating such an awesome project!

@advplyr
Copy link
Owner

advplyr commented Jun 25, 2022

I'm about half-way into implementing this and realized that we won't be able to use headers for static content on the server like images. For apps do you typically allow bypassing for static content? Like specifying a path to allow through.

@advplyr
Copy link
Owner

advplyr commented Jun 25, 2022

Another issue is the images for book covers and authors are going through the api routes.

Here is an example get request for a book cover image /api/items/li_rghidt3v6x9ef35sq3/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwiaWF0IjoxNjUyNTYyNDM5fQ

This is requested directly from an HTML image element and will cache and return the optimized image.

@rmkjr
Copy link
Author

rmkjr commented Jun 25, 2022

I think one could technically bypass tunnel authentication for particular URL locations, but that's not one that I've run into so far with the several apps I have routed that way.

I think ideally all requests to the server endpoint, including those for well-known static content, would add the additional request headers.

I've been out of the dev game for about a decade, so going to brush up against some knowledge edges here. It looks like you have the user token passed as an "Authorization" header in most cases. I think these extra headers would be appended basically every place that ABS user token is added to requests.

I also see the http img element for covers for example where it is setting the src to the known cover url. In those cases, does this url endpoint also bypass checking the ABS's own Authorization header? That is to say the /api endpoint (or maybe just /api/items) is open and unauthenticed? If that is true, then I think you'd be correct. Either the particular location (like /api/items) would have to bypass authentication tunnel side so it can remain unauthenticed, or those request would have to be refactored and be handled by a request method that can handle additional request headers rather than the inherent thing that is used to load an img tag's src.

@advplyr
Copy link
Owner

advplyr commented Jun 25, 2022

When authenticating for images ABS will check the query string token passed in the url.
That is why in the example request above for the image you see the token passed in the URL. This is also used when streaming, your token gets passed in as a query string when direct playing an mp3 file for example.

I'm not sure how to set custom headers for requests made through HTML elements, it may not be possible. Do these other services you use that offer this support serve images from your server? I would be curious if you could check the HTTP request or if they have source code to look at.

@rmkjr
Copy link
Author

rmkjr commented Jun 25, 2022

LunaSea, which aggregates as an app frontend for quite a few of the services I host out through these tunnels, does have similar static content loaded. For example, both Radarr and Sonarr have image covers for Movies and TV shows that the LunaSea app will pull from the respective sever and display.

I poked around its source a bit, but from the little I could interpret, it looks to be squarely in some overarching framework that may happen to have a request method that is capable of adding request headers. Guessing that means they're requesting the image content via those methods and then building their views rather than an img tag in a view loading it directly.

https://github.com/JagandeepBrar/LunaSea

@advplyr
Copy link
Owner

advplyr commented Jun 25, 2022

Yeah that is the only way to do it. We would need to write a custom image component as a wrapper around the img tag that would fetch the images first with the proper headers then use file blob in img src. I think it is common to do this so it shouldn't impact performance.
There are also images used in the player notification and in android auto that would need to be downloaded first.

@hskrtich
Copy link

I took a look at lunasea and how it integrate into other apps and I have a couple of thoughts.

It looks like loading static assets are not a problem since the the Cloudflare Tunnel will generate and set a JWT (cookie) on the first auth to the tunnel. So any other requests that come from the client should send the cookie along with the http request, which happens normally from most apps.

According to https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access
When a request is made to an application behind our network, the request will submit them both to Access. If the service token is valid, Access generates a JWT scoped to the application. All subsequent requests with that JWT will succeed until the expiration of that JWT.

As for putting this in front of ABS. Since this system is much more designed for apps that dont have there own user logins this isnt as useful as it could be since this basically puts an additional auth/login (the tunnel) in front of the app. Which works great for things like Radarr and such since they dont have any kind of user/login system. Its also a weird workflow when it comes to the clients trying to connect into the server since it would require another set of creds to be passed into the app.

@advplyr
Copy link
Owner

advplyr commented Jun 26, 2022

If that is the case then that could really simplify things with the images and stream requests. I think in order to test that I would need to set this up.

@Presjar
Copy link

Presjar commented Jul 14, 2022

An example what does this very well are all the 'arr' apps. Like Sonarr, Radarr, Prowlarr. and also SABnzbd. Each app has an API key (in the case of audiobookshelf, there would be an API key per user account).

I am then able to use NZB360 which passes the API key in the https headers. I use Traefik to catch this API key to allow access through the proxy and then login to the endpoint application.

If the API key is missing like when accessing not via the NZB360 app, It will use google oauth to authenticate the proxy.

`Snip from my dockerconfig

HTTP Routers Auth Bypass

  - "traefik.http.routers.prowlarr-rtr-bypass.entrypoints=https"
  - "traefik.http.routers.prowlarr-rtr-bypass.rule=Host(`prowlarr.$DOMAINNAME`) && (Headers(`X-Api-Key`, `$PROWLARR_API_KEY`) || Query(`apikey`, `$PROWLARR_API_KEY`))"
  - "traefik.http.routers.prowlarr-rtr-bypass.priority=100"

HTTP Routers Auth

  - "traefik.http.routers.prowlarr-rtr.entrypoints=https"
  - "traefik.http.routers.prowlarr-rtr.rule=Host(`prowlarr.$DOMAINNAME`)"
  - "traefik.http.routers.prowlarr-rtr.priority=99"

`

@advplyr
Copy link
Owner

advplyr commented Jul 21, 2022

The big question still is the static assets. If anyone is able to test what @bskrtich mentioned about the cookie then the header stuff is almost implemented for all the API requests, just not the static file requests like images and audio files

@zackyancey
Copy link

zackyancey commented Nov 12, 2022

I've got my audiobookshelf instance behind a reverse proxy with some auth middleware, how would I go about testing the app?

I'm using the android 9.59-beta version, but I don't see a button for custom headers on the add server screen. Do I need a different build with the feature enabled?

@advplyr
Copy link
Owner

advplyr commented Nov 12, 2022

To test this you would need to uncomment out this code block https://github.com/advplyr/audiobookshelf-app/blob/master/components/connection/ServerConnectForm.vue#L25
and build the app from source. That is just to show the button that allows you to add custom headers.

@hskrtich
Copy link

Id like to try to take a stab at auto showing a web login page if the server is behind a zero trust config. This would allow users to login how ever they need to via the zero trust and see the correct session headers with out the header http header field.

@advplyr
Copy link
Owner

advplyr commented Nov 12, 2022

I haven't gone deeply into the SSO & auth middleware world for self-hosting but it seems like the solutions being implemented are all over the place.

@Avsynthe
Copy link

For the most part, they're implementing main open standards utilised even in enterprise settings. Most commonly nowadays they implement using SAML or OIDC but similar apps like Calibre-Web, they simply read a header and combine it with LDAP for a more crude solution

Solutions like Authentik are available now where all of that is possible as it's pretty much All-in-one so we can pair up pretty much SSO choice made in any other app

@zackyancey
Copy link

I gave it a shot, but it wasn't able to connect to the server:

2022-11-13 09:56:25.057 9871-9871/com.audiobookshelf.app.debug I/Capacitor/Console: File: http://localhost/_nuxt/57a3121.js - Line 1 - Msg: [Axios] Making request to https://.../ping
2022-11-13 09:56:25.152 9871-9905/com.audiobookshelf.app.debug D/EGL_emulation: app_time_stats: avg=101.26ms min=6.50ms max=480.49ms count=10
2022-11-13 09:56:25.660 9871-9949/com.audiobookshelf.app.debug I/cr_X509Util: Failed to validate the certificate chain, error: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
2022-11-13 09:56:25.752 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/connect - Line 0 - Msg: Access to XMLHttpRequest at 'https://.../ping' from origin 'http://localhost' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
2022-11-13 09:56:25.754 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/_nuxt/57a3121.js - Line 1 - Msg: Axios error code NaN
2022-11-13 09:56:25.754 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/_nuxt/a8e4746.js - Line 1 - Msg: Server check failed Error: Network Error

I can get to the site in a web browser, so I don't think there's an HTTPS problem. Could the pre-flight request be being sent without the right headers? The auth provider will try to redirect to the sign in page if it doesn't see the right headers, so that might be the redirect in the log. I can curl /ping with the right headers manually and that part of it seems to work.

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

Would it be normal for the auth middleware to perform a redirect from a preflight request?

@zackyancey
Copy link

I don't know what's considered standard, but the one I'm using looks like it will redirect to the sign in page if you try to access any protected URL without being signed in.

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

I guess one way to test would be to run the server locally and add a console.log for the req here https://github.com/advplyr/audiobookshelf/blob/master/server/Auth.js#L20

Then disabling your auth middleware to see how the request come in. Unless you can add logs to your auth middleware to see what the request looks like.

@hskrtich
Copy link

hskrtich commented Nov 13, 2022

Yes, its normal to redirect any request including a preflight/OPTIONS request since at that point any request isnt authenticated. The header will need to get set for any request sent to the server.

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

I'm not sure why the headers wouldn't be included in the preflight request. I would assume they are but only way to find out for sure is to log the request.

@zackyancey
Copy link

zackyancey commented Nov 13, 2022

Here's a log for the request:

{
    "level": "info",
    "ts": 1668373973.5617716,
    "logger": "http.log.access.log1",
    "msg": "handled request",
    "request": {
        "remote_ip": "...",
        "remote_port": "...",
        "proto": "HTTP/2.0",
        "method": "OPTIONS",
        "host": "audiobookshelf.example.com",
        "uri": "/ping",
        "headers": {
            "Sec-Fetch-Mode": [
                "cors"
            ],
            "X-Requested-With": [
                "com.audiobookshelf.app.debug"
            ],
            "Sec-Fetch-Site": [
                "cross-site"
            ],
            "Access-Control-Request-Headers": [
                "x-api-key"
            ],
            "User-Agent": [
                "Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"
            ],
            "Origin": [
                "http://localhost"
            ],
            "Sec-Fetch-Dest": [
                "empty"
            ],
            "Referer": [
                "http://localhost/"
            ],
            "Accept-Encoding": [
                "gzip, deflate"
            ],
            "Accept-Language": [
                "en-US,en;q=0.9"
            ],
            "Accept": [
                "*/*"
            ],
            "Access-Control-Request-Method": [
                "GET"
            ]
        },
        "tls": {
            "resumed": false,
            "version": 772,
            "cipher_suite": 4865,
            "proto": "h2",
            "server_name": "audiobookshelf.example.com"
        }
    },
    "user_id": "",
    "duration": 0.00022408,
    "size": 5,
    "status": 302,
    "resp_headers": {
        "Server": [
            "Caddy"
        ],
        "Alt-Svc": [
            "h3=\":443\"; ma=2592000"
        ],
        "Location": [
            "(redirects to the auth portal)"
        ]
    }
}

X-API-Key is the header that's needed to get past the authentication. I see it mentioned in the Access-Control-Request-Headers header, but the key header itself is missing.

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

Ah I see. Were you able to verify that it is going into the regular request?

@zackyancey
Copy link

I didn't, but I can give that a shot.

@zackyancey
Copy link

Yeah, if I turn off the middleware but leave the custom header, it is present in the request itself but still not in the preflight.

{"level":"info","ts":1668375891.9092393,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"...","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/ping","headers":{"Accept":["*/*"],"Access-Control-Request-Headers":["x-api-key"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Access-Control-Request-Method":["GET"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"]}},"user_id":"","duration":0.002833663,"size":2,"status":200,"resp_headers":{"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Date":["Sun, 13 Nov 2022 21:44:51 GMT"],"X-Powered-By":["Express"],"Content-Length":["2"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["text/plain; charset=utf-8"],"Server":["Caddy"]}}
{"level":"info","ts":1668375891.9207191,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"...","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/ping","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"If-None-Match":["W/\"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA\""],"Accept":["application/json, text/plain, */*"],"X-Api-Key":["API_KEY"]}},"user_id":"","duration":0.002336694,"size":0,"status":304,"resp_headers":{"Access-Control-Allow-Origin":["*"],"Server":["Caddy"],"Access-Control-Allow-Credentials":["true"],"Etag":["W/\"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA\""],"Date":["Sun, 13 Nov 2022 21:44:51 GMT"],"X-Powered-By":["Express"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Headers":["*"]}}

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

I did some reading up and it seems like the auth middleware should not be requiring those headers in preflight requests.

The preflight request gets sent when there is a header in the request that isn't CORS safelisted. In your case that header is not safelisted so the preflight request gets sent out first to see if the request should be sent.
In that case it makes sense that the preflight request wouldn't include those headers since the preflight request is being sent to check if they are accepted.

The solutions to this preflight header issue I'm seeing are to fix the server to properly handle preflight requests.

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

Reading through this the server should probably not be redirecting a preflight request at all. Apparently it is now supported but not all browsers have made that change (not sure when that was written).

They do provide a possible solution which is to send a simple request first (request not requiring preflight) then get the redirect URL.

If that's not possible, then another way is to:

  1. Make a simple request (using Response.url for the Fetch API, or XMLHttpRequest.responseURL) to determine what URL the real preflighted request would end up at.
  2. Make another request (the real request) using the URL you obtained from Response.url or XMLHttpRequest.responseURL in the first step.

Update: actually I don't think that would be a solution for us since we need to continue sending those headers with all requests which would then get rejected by the server preflight everytime.

@zackyancey
Copy link

Hm, I wonder if that's something I can fix in my server configuration.

On the app side, I did some more testing with the auth disabled and I don't think that it's sending the token for image requests:

{"level":"info","ts":1668380662.7924142,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_yu52740ihsp4drvw61/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Authorization":[],"X-Api-Key":["..."],"X-Requested-With":["com.audiobookshelf.app.debug"],"Referer":["http://localhost/"],"Accept-Language":["en-US,en;q=0.9"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Accept":["application/json, text/plain, */*"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Origin":["http://localhost"]}},"user_id":"","duration":0.022020836,"size":29757,"status":200,"resp_headers":{"X-Powered-By":["Express"],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["application/json; charset=utf-8"],"Content-Length":["29757"],"Date":["Sun, 13 Nov 2022 23:04:22 GMT"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Etag":["W/\"743d-PyVlnv+/nY4QEAXE7pwQmFD6V/E\""]}}
{"level":"info","ts":1668380738.3990152,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx?include=filterdata","headers":{"Access-Control-Request-Headers":["authorization,x-api-key"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Accept":["*/*"],"Access-Control-Request-Method":["GET"]}},"user_id":"","duration":0.000878268,"size":2,"status":200,"resp_headers":{"Server":["Caddy"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Headers":["*"],"Content-Type":["text/plain; charset=utf-8"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Length":["2"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"X-Powered-By":["Express"]}}
{"level":"info","ts":1668380738.4180558,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx?include=filterdata","headers":{"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Authorization":[],"X-Api-Key":["..."],"Connection":["keep-alive"],"Accept":["application/json, text/plain, */*"],"Origin":["http://localhost"],"Referer":["http://localhost/"]}},"user_id":"","duration":0.001421874,"size":576,"status":200,"resp_headers":{"Etag":["W/\"240-KzyrH3qKuqhGQKwddXPtonvTIC0\""],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Content-Type":["application/json; charset=utf-8"],"Content-Length":["576"],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"X-Powered-By":["Express"]}}
{"level":"info","ts":1668380738.4455457,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Accept":["*/*"],"Access-Control-Request-Headers":["authorization,x-api-key"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Access-Control-Request-Method":["GET"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"]}},"user_id":"","duration":0.00140514,"size":2,"status":200,"resp_headers":{"Server":["Caddy"],"Access-Control-Allow-Headers":["*"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["text/plain; charset=utf-8"],"Content-Length":["2"],"X-Powered-By":["Express"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"Access-Control-Allow-Origin":["*"]}}
{"level":"info","ts":1668380738.453754,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Connection":["keep-alive"],"X-Api-Key":["..."],"Origin":["http://localhost"],"Referer":["http://localhost/"],"Accept":["application/json, text/plain, */*"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Authorization":[],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"]}},"user_id":"","duration":0.002885038,"size":1086,"status":200,"resp_headers":{"Access-Control-Allow-Headers":["*"],"Content-Type":["application/json; charset=utf-8"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Content-Length":["1086"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Credentials":["true"],"X-Powered-By":["Express"],"Etag":["W/\"43e-U/Os9cVnBKrVPxqyec91eukucCE\""]}}
{"level":"info","ts":1668380738.4691706,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40886","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwidXNlcm5hbWUiOiJyb290IiwiaWF0IjoxNjYwNDExOTYwfQ.-MgjHunuacvRMgukmb3Kc5s7ZVI0enTybpsIodCsG1w&ts=1661008790276","headers":{"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Referer":["http://localhost/"],"Accept":["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"]}},"user_id":"","duration":0.003245206,"size":16742,"status":200,"resp_headers":{"Access-Control-Allow-Credentials":["true"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"X-Powered-By":["Express"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Server":["Caddy"],"Content-Type":["image/webp"],"Access-Control-Allow-Headers":["*"]}}

The first few lines have the token for the GETs and not on the OPTIONSs as we'd expect, but the last line is getting a cover and doesn't have the token.

@advplyr
Copy link
Owner

advplyr commented Nov 13, 2022

Yeah for images and other requests that are happening in HTML tags the token is put in as a query string /api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

That is actually what my original question about cookies was about. There are some requests we want to make from an HTML img or audio tag that we won't be able to easily attach headers to. We discussed this above #254 (comment)

@zackyancey
Copy link

Ah, I see, so I'll need to get the preflight redirect issues worked out on my middleware before I can test that then.

@zackyancey
Copy link

Ok, so I can turn off the authentication for OPTIONS requests, and now I can log in. Covers and streams don't work though. I would guess that the middleware might not be providing a cookie when authenticating with an API key—it's meant for API access after all, not browsing. You probably only get the cookie if you go through the login page.

In my case this could probably be solved with the method @hskrtich mentioned of bringing up a sign in page to authenticate and get the cookie (don't JWTs usually come with an expiration date though? Would you have to sign in periodically in that case?). I don't think it would work in all cases—another simple way you can expose a self-hosted service is with HTTPS+basic auth. With that setup I don't think there'd be any way other than getting the headers into the image/stream requests.

@hskrtich
Copy link

Yes the JWT do expire. The pop up log in window would need to be shown anytime it redirects to do auth.

@advplyr
Copy link
Owner

advplyr commented Nov 14, 2022

I'm not sure that cookies are going to work. Maybe it will work with the requests coming out of webview but requests are also sent natively in the Kotlin/Swift code.
Images and streams are all going through the same endpoints so a temporary solution could be to whitelist those endpoints.

@hskrtich
Copy link

Web session will need to be saved (which include cookies) and used for both the native requests and webviews. Something like this
https://stackoverflow.com/questions/11224454/android-share-session-between-webview-and-httpclient

@Keavon
Copy link

Keavon commented Apr 22, 2023

Hello, thanks for working on this! Any updates?

@Gathaeryx
Copy link

Is this issue still being worked on? Safe remote access would be great to have if you want to access your library on the go.

@rweatherly
Copy link

i wanted to check in on this issue, as I am attempting to move my ABS server from its current setup behind HA Proxy to being setup with access through Cloudflare tunnel. The Website/wepapp to my endpoint works and I can login, but the mobile app fails because of a redirect.

If there is any info i can add to help with this issue please let me know

@edutchie
Copy link

edutchie commented Jun 6, 2024

This issue affects my ABS deployment as well; it can only be accessed through Cloudflare tunnel. I cannot connect through the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

10 participants