Unable to access ODK Central via ajax calls

#1

1. What is the problem? Be very detailed.
I installed ODK Central on a Digital Ocean droplet following this howto:

https://docs.opendatakit.org/central-install-digital-ocean/

and everything works great out of the box. We've created new users, new surveys, etc. and all is good.

I'm working on a data visualization front-end that uses ODK Central's odata interface and I'd like to write it in javaScript to make it most portable.

I'm trying to enable making ajax calls to ODK central using axios. I've attempted enabling CORS on my server (details below).

However, when I make an ajax call in axios like so:

    var api_url = "https://mydomain.com/v1/sessions";
    var config = {
        headers: {
            'Accept': 'application/json, text/plain, */*',
            'Content-Type': 'application/json;charset=utf-8'
        }
    };

    axios.post(api_url, {
        email: 'troy@troyvit.com',
        password: '[PASSWORD]'
    }, config)
    .then(function (response) {
        console.log(response);
    })
    .catch(function (error) {
        console.log(error);
    });

I receive this:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://data.troyvit.net/v1/sessions. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).[Learn More]

Additionally I receive a 404 error. I get the same error when I try to make the call from curl:

curl -H "Origin: https://example.com"   -H "Access-Control-Request-Method: POST"   -H "Access-Control-Request-Headers: X-Requested-With"   -X OPTIONS --verbose   https://mydomain.com/v1/sessions

[...]

< HTTP/1.1 404 Not Found
< Server: nginx
< Date: Sat, 13 Apr 2019 17:50:48 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 76
< Connection: keep-alive
< X-Powered-By: Express
< ETag: W/"4c-ShTrjQvXJDA49Wfxw7ES7C6cQFg"
< 
* Connection #0 to host data.troyvit.net left intact
{"message":"Could not find the resource you were looking for.","code":404.1}

2. What app or server are you using and on what device and operating system? Include version numbers.

I installed ODK Central on a Digital Ocean droplet following this howto:

https://docs.opendatakit.org/central-install-digital-ocean/

I'm running ODK Central v0.4 Beta and nginx/1.10.3

My files/nginx/odk.conf.template looks like this:

server {
  listen 443 ssl;
  ssl_certificate /etc/${SSL_TYPE}/live/${DOMAIN}/fullchain.pem;
  ssl_certificate_key /etc/${SSL_TYPE}/live/${DOMAIN}/privkey.pem;
  ssl_trusted_certificate /etc/${SSL_TYPE}/live/${DOMAIN}/fullchain.pem;

  ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
  ssl_dhparam /etc/dh/nginx.pem;

  server_tokens off;
  add_header Strict-Transport-Security "max-age=31536000";
  add_header X-Content-Type-Options nosniff;

  client_max_body_size 100m;

  gzip on;
  gzip_vary on;
  gzip_min_length 1280;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/x-javascript text/xml text/csv;

  location ~ ^/v\d {
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://service:8383;
    proxy_redirect off;

    # set up request-body gzip decompression:
    set $max_chunk_size 16384;    # ~16KB
    set $max_body_size 134217728; # ~128MB
    rewrite_by_lua_file inflate_body.lua;
  }

  location / {
    root /usr/share/nginx/html;
        # Wide-open CORS config for nginx
        # Copied from https://enable-cors.org/server_nginx.html
       if ($request_method = 'OPTIONS') {
          add_header 'Access-Control-Allow-Origin' '*';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          #
          # Custom headers and headers various browsers *should* be OK with but aren't
          #
          add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
          #
          # Tell client that this pre-flight info is valid for 20 days
          #
          add_header 'Access-Control-Max-Age' 1728000;
          add_header 'Content-Type' 'text/plain; charset=utf-8';
          add_header 'Content-Length' 0;
          return 204;
       }
       if ($request_method = 'POST') {
          add_header 'Access-Control-Allow-Origin' '*';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
          add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
       }
       if ($request_method = 'GET') {
          add_header 'Access-Control-Allow-Origin' '*';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
          add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
       }
  }
}

I rebuilt my container it it looks like the configration change worked:

root@ce6ccd378540:/etc/nginx/conf.d# cat odk.conf 
server {
  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/data.troyvit.net/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/data.troyvit.net/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/data.troyvit.net/fullchain.pem;

  ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
  ssl_dhparam /etc/dh/nginx.pem;

  server_tokens off;
  add_header Strict-Transport-Security "max-age=31536000";
  add_header X-Content-Type-Options nosniff;

  client_max_body_size 100m;

  gzip on;
  gzip_vary on;
  gzip_min_length 1280;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/x-javascript text/xml text/csv;

  location ~ ^/v\d {
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://service:8383;
    proxy_redirect off;

    # set up request-body gzip decompression:
    set $max_chunk_size 16384;    # ~16KB
    set $max_body_size 134217728; # ~128MB
    rewrite_by_lua_file inflate_body.lua;
  }

  location / {
    root /usr/share/nginx/html;
        # Wide-open CORS config for nginx
        # Copied from https://enable-cors.org/server_nginx.html
       if ($request_method = 'OPTIONS') {
          add_header 'Access-Control-Allow-Origin' '*';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          #
          # Custom headers and headers various browsers *should* be OK with but aren't
          #
          add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
          #
          # Tell client that this pre-flight info is valid for 20 days
          #
          add_header 'Access-Control-Max-Age' 1728000;
          add_header 'Content-Type' 'text/plain; charset=utf-8';
          add_header 'Content-Length' 0;
          return 204;
       }
       if ($request_method = 'POST') {
          add_header 'Access-Control-Allow-Origin' '*';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
          add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
       }
       if ($request_method = 'GET') {
          add_header 'Access-Control-Allow-Origin' '*';
          add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
          add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
          add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
       }
  }
}

nginx seems to like it:

/usr/sbin/nginx -t
2019/04/13 17:27:11 [info] 720#720: Using 32768KiB of shared memory for nchan in /etc/nginx/nginx.conf:64
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

nginx.conf includes everything in conf.d and I'm assuming naively that when you run a config test it includes the files in conf.d. Given that nginx starts up cleanly I'm fairly certain the config is ok.

3. What you have you tried to fix the problem?
I've successfully made server-side calls to /v1/sessions using PHP, and I've successfully made the same ajax calls to other CORS-enabled domains using the same script.

I also moved the CORS location {} code into /etc/nginx/sites-enabled/default and placed it within a server{} directive. There was no change in the headers the server returns.

4. What steps can we take to reproduce the problem?
Copy the config template settings above and put them into a container and then try to log in.

Thanks in advance for any ideas you might have.

0 Likes

#2

so i think you actually want to either move or copy your CORS directives up to the location ~ ^/v\d {} block, since that's the block that actually manages the API itself. give that a try and let me know how it goes?

1 Like

#3

in general also it might be a good idea for us to enable CORS for OData requests, but i haven't thought through whether there would be security reasons some users wouldn't want that on. i'll think about it some.

0 Likes

#4

That was it! Man I can't believe I missed that location directive. I copied the CORS headers to that and it worked like a charm.

I agree about the security issues. I think having Access-Control-Allow-Origin' wide open is not a good default config.

What would it be like to add a flag to .env to give the option to lock it down to a domain? I'm pretty clumsy with gitHub but if you think that's a good idea I'd be happy to make a pull request and add a variable to .env and the CORS info to odk.conf.template to support it.

Thanks for the rapid help. ODK is great.

2 Likes