Opentrons / opentrons

Software for writing protocols and running them on the Opentrons Flex and Opentrons OT-2
https://opentrons.com
Apache License 2.0
425 stars 179 forks source link

feat: Adding "CORS" policy to opentrons APP #7599

Closed beniroquai closed 3 years ago

beniroquai commented 3 years ago

Overview

We are trying to interact with the opentrons API remotely and were already very successful with sending requests through python. Now we would like to integrate the API into the openflexure server system using the imjoy.io plugin environment. Wei, the creator of ImJoy and I tried creating a simple toggle-button plugin:

image

to remote control the opentrons within the plugin. The relevant code snippet is this here (a javascript post request as a ImJoy plugin):

<docs lang="markdown">
[TODO: write documentation for this plugin.]
</docs>
<config lang="json">
{
  "name": "OpentronsDemo",
  "type": "window",
  "tags": [],
  "ui": "",
  "version": "0.1.2",
  "api_version": "0.1.5",
  "description": "This is a backbone plugin for using Vue.js in ImJoy",
  "icon": "extension",
  "inputs": null,
  "outputs": null,
  "env": "",
  "requirements": [
      "https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.22/vue.min.js",
      "https://static.imjoy.io/spectre.css/spectre.min.css",
      "https://static.imjoy.io/spectre.css/spectre-exp.min.css",
      "https://static.imjoy.io/spectre.css/spectre-icons.min.css"
  ],
  "dependencies": [],
  "defaults": {"w": 20, "h": 10},
  "runnable": true,
  "base_frame": "http://21.3.2.3:5000/baseframe.html"
}
</config>
<script lang="javascript">
const app = new Vue({
  el: '#app',
  data: {
    title: 'Opentrons Demo'
  },
  methods: {
    async toggleLight(){
      const payload = {'on': 'true' }
      const response = await fetch(
        'http://21.3.2.11:31950/robot/lights', 
        { method: 'POST',
          headers: {'opentrons-version': '*'},
          body: JSON.stringify(payload)
        })
      const data = await response.json()
      console.log(data)
    }
  }
})
class ImJoyPlugin {
  async setup() {
  }
  async run(my) {
  }
}
api.export(new ImJoyPlugin())
</script>
<window lang="html">
  <div style="text-align: center;" id="app">
    <h1>{{title}}</h1>
    <button class="btn" @click="toggleLight()">Toggle Light</button>
  </div>
</window>
<style lang="css">
</style>

When executing the plugin and sending the POST, we get the following error:

baseframe.html:1 Access to fetch at 'http://21.3.2.11:31950/robot/lights' from origin 'http://21.3.2.3:5000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Implementation details

Wei suggested to implement the following fix inside the app.py (opentrons/robot-server/robot_server/service/app.py):

import logging
import traceback

from fastapi.middleware.cors import CORSMiddleware
from opentrons import __version__
from fastapi import FastAPI, APIRouter, Depends
from fastapi.exceptions import RequestValidationError
from starlette.responses import Response, JSONResponse
from starlette.requests import Request
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.status import (
    HTTP_422_UNPROCESSABLE_ENTITY
)

from .logging import initialize_logging
from robot_server.service.legacy.models import V1BasicResponse
from .errors import V1HandlerError, \
    transform_http_exception_to_json_api_errors, \
    transform_validation_error_to_json_api_errors, \
    consolidate_fastapi_response, BaseRobotServerError, ErrorResponse, \
    build_unhandled_exception_response
from .dependencies import (
    get_rpc_server, get_protocol_manager, get_hardware_wrapper,
    verify_hardware, get_session_manager, check_version_header)
from robot_server import constants
from robot_server.service.legacy.routers import legacy_routes
from robot_server.service.session.router import router as session_router
from robot_server.service.pipette_offset.router import router as pip_os_router
from robot_server.service.labware.router import router as labware_router
from robot_server.service.protocol.router import router as protocol_router
from robot_server.service.system.router import router as system_router
from robot_server.service.tip_length.router import router as tl_router
from robot_server.service.notifications.router import router as \
    notifications_router

log = logging.getLogger(__name__)

app = FastAPI(
    title="Opentrons OT-2 HTTP API Spec",
    description="This OpenAPI spec describes the HTTP API of the Opentrons "
                "OT-2. It may be retrieved from a robot on port 31950 at "
                "/openapi. Some schemas used in requests and responses use "
                "the `x-patternProperties` key to mean the JSON Schema "
                "`patternProperties` behavior.",
    version=__version__,
)

# disable CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://lib.imjoy.io",
        "http://21.3.2.3:5000",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["Content-Type", "Authorization"],
)

in order to disable CORS.

Do you think this is possible? Or is this a very user-specific case?

Design

-

Acceptance criteria

Solved, if we can remote control the opentrons through an ImJoy plugin.

mcous commented 3 years ago

@beniroquai I think you noticed this issue right at the same time we noticed! We believe HTTP API should be interactable from any origin; the lack of CORS middleware was on oversight.

7632 includes your requested change with an allow_origins value of *. ~We may be able to get this into the upcoming 4.3 release~ unfortunately missed the window on 4.3, but it'll certainly by 4.4

beniroquai commented 3 years ago

One more update on this issue (just for brevity). We were able to use ImJoy inside the Opentrons Jupyter notebook:

image

With this you can use all kinds of machine learning plugins :-)

Closing the issue for now (or was already closed..nice!:)