├── tests ├── __init__.py ├── test_subscribe_to_core.py ├── test_subscribe_topic_handler.py └── test_handle_message.py ├── src ├── frontend │ ├── .dockerignore │ ├── public │ │ ├── robots.txt │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── setupTests.js │ │ ├── App.test.js │ │ ├── App.css │ │ ├── index.css │ │ ├── reportWebVitals.js │ │ ├── index.js │ │ └── App.js │ ├── req.conf │ ├── Dockerfile │ ├── package.json │ ├── nginx.default.conf │ ├── .gitignore │ └── README.md ├── backend │ ├── requirements.txt │ ├── Dockerfile │ └── app.py └── docker-compose.yaml ├── docs ├── login.png ├── authenticated.png └── aws.greengrass.labs.LocalWebServer.png ├── CODE_OF_CONDUCT.md ├── req.conf ├── gdk-config.json ├── recipe.yaml ├── CONTRIBUTING.md ├── .gitignore ├── README.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /src/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | awsiotsdk==1.7.1 2 | flask-login==0.5.0 3 | flask-socketio==5.1.1 4 | -------------------------------------------------------------------------------- /docs/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-greengrass-labs-local-web-server/HEAD/docs/login.png -------------------------------------------------------------------------------- /src/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/authenticated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-greengrass-labs-local-web-server/HEAD/docs/authenticated.png -------------------------------------------------------------------------------- /docs/aws.greengrass.labs.LocalWebServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-greengrass-labs-local-web-server/HEAD/docs/aws.greengrass.labs.LocalWebServer.png -------------------------------------------------------------------------------- /src/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } -------------------------------------------------------------------------------- /src/frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim-buster 2 | COPY requirements.txt ./ 3 | 4 | RUN apt-get update && apt-get install -y cmake 5 | RUN pip install -r ./requirements.txt 6 | 7 | COPY app.py ./ 8 | 9 | CMD ["python", "app.py"] 10 | 11 | RUN adduser --system backend 12 | USER backend -------------------------------------------------------------------------------- /src/frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /req.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | distinguished_name = req_distinguished_name 4 | prompt = no 5 | 6 | [req_distinguished_name] 7 | countryName = US 8 | stateOrProvinceName = Washington 9 | localityName = Seattle 10 | organizationName = Amazon.com 11 | organizationalUnitName = Amazon Web Services 12 | commonName = localhost 13 | 14 | [v3_ca] 15 | subjectAltName = @alt_names 16 | 17 | [alt_names] 18 | DNS.1 = localhost -------------------------------------------------------------------------------- /src/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: left; 3 | } 4 | 5 | .App-logout { 6 | text-align: right; 7 | } 8 | 9 | .App-header { 10 | background-color: #282c34; 11 | min-height: 100vh; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | font-size: calc(10px + 2vmin); 17 | color: white; 18 | } 19 | 20 | .App-link { 21 | color: #61dafb; 22 | } 23 | -------------------------------------------------------------------------------- /src/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/frontend/req.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | distinguished_name = req_distinguished_name 4 | prompt = no 5 | 6 | [req_distinguished_name] 7 | countryName = US 8 | stateOrProvinceName = Washington 9 | localityName = Seattle 10 | organizationName = Amazon.com 11 | organizationalUnitName = Amazon Web Services 12 | commonName = localhost 13 | 14 | [v3_ca] 15 | subjectAltName = @alt_names 16 | 17 | [alt_names] 18 | DNS.1 = localhost 19 | -------------------------------------------------------------------------------- /src/frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /gdk-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": { 3 | "aws.greengrass.labs.LocalWebServer": { 4 | "author": "Amazon", 5 | "version": "NEXT_PATCH", 6 | "build": { 7 | "build_system": "custom", 8 | "custom_build_command": [ 9 | "bash", 10 | "build-custom.sh", 11 | "aws.greengrass.labs.LocalWebServer", 12 | "NEXT_PATCH" 13 | ] 14 | }, 15 | "publish": { 16 | "bucket": "", 17 | "region": "" 18 | } 19 | } 20 | }, 21 | "gdk_version": "1.0.0" 22 | } -------------------------------------------------------------------------------- /src/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | backend: 4 | build: ./backend 5 | image: flask-app 6 | expose: 7 | - "5000" 8 | volumes: 9 | - /greengrass/v2:/greengrass/v2 10 | environment: 11 | - AWS_REGION 12 | - SVCUID 13 | - AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH_FOR_COMPONENT 14 | - AWS_CONTAINER_AUTHORIZATION_TOKEN 15 | - AWS_CONTAINER_CREDENTIALS_FULL_URI 16 | - AWS_IOT_THING_NAME 17 | frontend: 18 | build: ./frontend 19 | image: react-webapp 20 | ports: 21 | - "3000:3001" 22 | volumes: 23 | - /greengrass/v2:/greengrass/v2 -------------------------------------------------------------------------------- /tests/test_subscribe_to_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import Flask 3 | from flask_socketio import SocketIO, send 4 | from artifacts.backend.app import subscribe_to_core 5 | 6 | 7 | def test_subscribe_to_core(mocker, monkeypatch): 8 | """Subscribe to AWS IoT Core for relaying to Socket.IO""" 9 | 10 | monkeypatch.setenv("AWS_IOT_THING_NAME", "TestDevice") 11 | ipc_connect = mocker.patch("awsiot.greengrasscoreipc.connect") 12 | 13 | app = Flask(__name__) 14 | socket_io = SocketIO(app, cors_allowed_origins="*") 15 | 16 | subscribe_to_core(socket_io) 17 | 18 | ipc_connect.assert_called_once() 19 | -------------------------------------------------------------------------------- /src/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import 'bootstrap/dist/css/bootstrap.css'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /tests/test_subscribe_topic_handler.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from flask import Flask 4 | from flask_socketio import SocketIO, send 5 | import awsiot.greengrasscoreipc.model as model 6 | from artifacts.backend.app import SubscribeTopicHandler 7 | 8 | 9 | def test_subscribe_topic_handler(mocker, monkeypatch): 10 | """Subscribe to AWS IoT Core for relaying to Socket.IO""" 11 | 12 | app = Flask(__name__) 13 | socket_io = SocketIO(app, cors_allowed_origins="*") 14 | 15 | stream_handler = SubscribeTopicHandler(socket_io) 16 | 17 | event = model.IoTCoreMessage( 18 | message=model.MQTTMessage( 19 | topic_name=None, payload=json.dumps({"test": "test"}).encode() 20 | ) 21 | ) 22 | 23 | stream_handler.on_stream_event(event) 24 | -------------------------------------------------------------------------------- /src/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.16.0-slim AS builder 2 | RUN npm install -g react-scripts --silent 3 | 4 | COPY . . 5 | 6 | RUN yarn install 7 | RUN yarn run build 8 | 9 | FROM nginx:stable 10 | 11 | COPY --from=builder /build /usr/share/nginx/html 12 | COPY nginx.default.conf /etc/nginx/conf.d/default.conf 13 | 14 | COPY req.conf / 15 | 16 | RUN mkdir -p /certs 17 | COPY ssl.crt /certs 18 | COPY ssl.key /certs 19 | 20 | RUN chown -R nginx:nginx /usr/share/nginx/html && chmod -R 755 /usr/share/nginx/html && \ 21 | chown -R nginx:nginx /certs && \ 22 | chown -R nginx:nginx /var/cache/nginx && \ 23 | chown -R nginx:nginx /var/log/nginx && \ 24 | chown -R nginx:nginx /etc/nginx/conf.d 25 | RUN touch /var/run/nginx.pid && \ 26 | chown -R nginx:nginx /var/run/nginx.pid 27 | 28 | USER nginx -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws.greengrass.labs.LocalWebServer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "axios": "0.24.0", 10 | "bootstrap": "^5.1.3", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-scripts": "4.0.3", 14 | "socket.io-client": "4.2.0", 15 | "web-vitals": "^1.0.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /src/frontend/nginx.default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3001 ssl; 3 | server_name localhost; 4 | ssl_certificate /certs/ssl.crt; 5 | ssl_certificate_key /certs/ssl.key; 6 | 7 | root /usr/share/nginx/html; 8 | index index.html; 9 | error_page 500 502 503 504 /50x.html; 10 | 11 | location / { 12 | try_files $uri $uri/ =404; 13 | add_header Cache-Control "no-cache"; 14 | } 15 | 16 | location /static { 17 | expires 1y; 18 | add_header Cache-Control "public"; 19 | } 20 | 21 | location /socket.io { 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header Host $host; 24 | 25 | proxy_http_version 1.1; 26 | proxy_buffering off; 27 | proxy_set_header Upgrade $http_upgrade; 28 | proxy_set_header Connection "Upgrade"; 29 | proxy_pass http://backend:5000/socket.io; 30 | } 31 | 32 | location /api { 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header Host $host; 35 | 36 | proxy_http_version 1.1; 37 | proxy_buffering off; 38 | proxy_set_header Upgrade $http_upgrade; 39 | proxy_set_header Connection "Upgrade"; 40 | proxy_pass http://backend:5000/api; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | scripts/flow/*/.flowconfig 4 | .flowconfig 5 | *~ 6 | *.pyc 7 | .grunt 8 | _SpecRunner.html 9 | __benchmarks__ 10 | build/ 11 | remote-repo/ 12 | coverage/ 13 | .module-cache 14 | fixtures/dom/public/react-dom.js 15 | fixtures/dom/public/react.js 16 | test/the-files-to-test.generated.js 17 | *.log* 18 | chrome-user-data 19 | *.sublime-project 20 | *.sublime-workspace 21 | .idea 22 | *.iml 23 | .vscode 24 | *.swp 25 | *.swo 26 | *.tar 27 | 28 | packages/react-devtools-core/dist 29 | packages/react-devtools-extensions/chrome/build 30 | packages/react-devtools-extensions/chrome/*.crx 31 | packages/react-devtools-extensions/chrome/*.pem 32 | packages/react-devtools-extensions/firefox/build 33 | packages/react-devtools-extensions/firefox/*.xpi 34 | packages/react-devtools-extensions/firefox/*.pem 35 | packages/react-devtools-extensions/shared/build 36 | packages/react-devtools-extensions/.tempUserDataDir 37 | packages/react-devtools-inline/dist 38 | packages/react-devtools-shell/dist 39 | packages/react-devtools-scheduling-profiler/dist 40 | .idea/.gitignore 41 | .idea/aws.xml 42 | .idea/codeStyles/ 43 | .idea/inspectionProfiles/ 44 | .idea/misc.xml 45 | .idea/modules.xml 46 | .idea/sg2ui.iml 47 | .idea/vcs.xml 48 | node_modules/ 49 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 50 | 51 | # dependencies 52 | /node_modules 53 | /.pnp 54 | .pnp.js 55 | 56 | # testing 57 | /coverage 58 | 59 | # production 60 | /build 61 | 62 | # misc 63 | .DS_Store 64 | .env.local 65 | .env.development.local 66 | .env.test.local 67 | .env.production.local 68 | 69 | npm-debug.log* 70 | yarn-debug.log* 71 | yarn-error.log* 72 | 73 | yarn.lock 74 | -------------------------------------------------------------------------------- /src/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | greengrass-v2-local-web-server 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /recipe.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | RecipeFormatVersion: "2020-01-25" 3 | ComponentName: "aws.greengrass.labs.LocalWebServer" 4 | ComponentVersion: "1.0.0" 5 | ComponentDescription: Component to demonstrate running Local Web Server on Greengrass v2 (with Flask-Socket.io, and React) 6 | ComponentPublisher: Amazon 7 | ComponentDependencies: 8 | aws.greengrass.Nucleus: 9 | VersionRequirement: ">=2.4.0" 10 | aws.greengrass.SecretManager: 11 | VersionRequirement: ">=2.0.9" 12 | ComponentConfiguration: 13 | DefaultConfiguration: 14 | accessControl: 15 | aws.greengrass.ipc.mqttproxy: 16 | "aws.greengrass.labs.LocalWebServer:pub:0": 17 | policyDescription: Allows access to publish to IoT Core topic(s). 18 | operations: 19 | - aws.greengrass#PublishToIoTCore 20 | resources: 21 | - "*" 22 | "aws.greengrass.labs.LocalWebServer:sub:0": 23 | policyDescription: Allows access to subscribe to IoT Core topic(s). 24 | operations: 25 | - aws.greengrass#SubscribeToIoTCore 26 | resources: 27 | - "*" 28 | aws.greengrass.ipc.pubsub: 29 | "aws.greengrass.labs.LocalWebServer:pub:1": 30 | policyDescription: Allows access to publish to local topics. 31 | operations: 32 | - aws.greengrass#PublishToTopic 33 | resources: 34 | - "*" 35 | "aws.greengrass.labs.LocalWebServer:sub:1": 36 | policyDescription: Allows access to subscribe to local topics. 37 | operations: 38 | - aws.greengrass#SubscribeToTopic 39 | resources: 40 | - "*" 41 | aws.greengrass.SecretManager: 42 | "aws.greengrass.labs.LocalWebServer:secrets:1": 43 | policyDescription: Allows access to Secret Manager values 44 | operations: 45 | - "aws.greengrass#GetSecretValue" 46 | resources: 47 | - "*" 48 | Manifests: 49 | - Platform: 50 | os: linux 51 | Lifecycle: 52 | Install: 53 | RequiresPrivilege: true 54 | Script: | 55 | docker load -i {artifacts:decompressedPath}/aws.greengrass.labs.LocalWebServer/custom-build/aws.greengrass.labs.LocalWebServer/react-webapp.tar && docker load -i {artifacts:decompressedPath}/aws.greengrass.labs.LocalWebServer/custom-build/aws.greengrass.labs.LocalWebServer/flask-app.tar 56 | Run: 57 | RequiresPrivilege: true 58 | Script: | 59 | docker-compose -f {artifacts:decompressedPath}/aws.greengrass.labs.LocalWebServer/custom-build/aws.greengrass.labs.LocalWebServer/docker-compose.yaml up --no-build 60 | Artifacts: 61 | - URI: s3://BUCKET_NAME/COMPONENT_NAME/COMPONENT_VERSION/aws.greengrass.labs.LocalWebServer.zip 62 | Unarchive: ZIP 63 | -------------------------------------------------------------------------------- /tests/test_handle_message.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from artifacts.backend.app import app, socket_io, get_secret 4 | 5 | 6 | def test_api_endpoints(mocker, monkeypatch): 7 | """Test Flask API endpoints""" 8 | 9 | # log the user in through Flask test client 10 | flask_test_client = app.test_client() 11 | 12 | # log in via HTTP 13 | r = flask_test_client.post( 14 | "/api/login", json={"username": "invalid", "password": "invalid"} 15 | ) 16 | assert r.status_code == 200 17 | assert json.loads(r.data.decode())["authenticated"] == False 18 | 19 | # log in via HTTP 20 | r = flask_test_client.post( 21 | "/api/login", json={"username": "test", "password": "test"} 22 | ) 23 | assert r.status_code == 200 24 | assert json.loads(r.data.decode())["authenticated"] == True 25 | 26 | # log in via HTTP 27 | r = flask_test_client.get("/api/authenticated") 28 | assert r.status_code == 200 29 | assert json.loads(r.data.decode())["authenticated"] == True 30 | 31 | # log in via HTTP 32 | r = flask_test_client.get("/api/logout") 33 | assert r.status_code == 200 34 | assert json.loads(r.data.decode())["authenticated"] == False 35 | 36 | 37 | def test_handle_message(mocker, monkeypatch): 38 | """Test relaying message back to MQTT""" 39 | 40 | # log the user in through Flask test client 41 | flask_test_client = app.test_client() 42 | 43 | # connect to Socket.IO without being logged in 44 | socketio_test_client = socket_io.test_client( 45 | app, flask_test_client=flask_test_client 46 | ) 47 | 48 | # make sure the server rejected the connection 49 | assert not socketio_test_client.is_connected() 50 | 51 | # log in via HTTP 52 | r = flask_test_client.post( 53 | "/api/login", json={"username": "test", "password": "test"} 54 | ) 55 | assert r.status_code == 200 56 | assert json.loads(r.data.decode())["authenticated"] == True 57 | 58 | # connect to Socket.IO again, but now as a logged in user 59 | socketio_test_client = socket_io.test_client( 60 | app, flask_test_client=flask_test_client 61 | ) 62 | 63 | # make sure the server accepted the connection 64 | r = socketio_test_client.get_received() 65 | assert len(r) == 1 66 | 67 | monkeypatch.setenv("AWS_IOT_THING_NAME", "TestDevice") 68 | ipc_connect = mocker.patch("awsiot.greengrasscoreipc.connect") 69 | 70 | socketio_test_client.emit("publish", "test") 71 | r = socketio_test_client.get_received() 72 | 73 | ipc_connect.assert_called_once() 74 | 75 | 76 | def test_get_secret(mocker, monkeypatch): 77 | """Test get_secret from Secrets Manager""" 78 | 79 | monkeypatch.setenv("AWS_IOT_THING_NAME", "TestDevice") 80 | ipc_connect = mocker.patch("awsiot.greengrasscoreipc.connect") 81 | mocker.patch("json.loads", return_value={"username": "test", "password": "test"}) 82 | 83 | get_secret() 84 | 85 | ipc_connect.assert_called_once() 86 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/backend/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | from datetime import timedelta 6 | 7 | import awsiot.greengrasscoreipc 8 | import awsiot.greengrasscoreipc.client as client 9 | import awsiot.greengrasscoreipc.model as model 10 | from flask import Flask, jsonify, request 11 | from flask_login import LoginManager, UserMixin, current_user, login_user, logout_user 12 | from flask_socketio import SocketIO, disconnect, send 13 | 14 | # Setup logging to stdout 15 | logger = logging.getLogger(__name__) 16 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 17 | 18 | app = Flask(__name__) 19 | app.config.update( 20 | DEBUG=True, 21 | SECRET_KEY="greengrass-v2-example", 22 | PERMANENT_SESSION_LIFETIME=timedelta(minutes=15), 23 | ) 24 | login_manager = LoginManager() 25 | login_manager.init_app(app) 26 | TIMEOUT = 10 27 | socket_io = SocketIO(app, cors_allowed_origins="*") 28 | users = [{"id": 46, "username": "test", "password": "test"}] 29 | 30 | 31 | class User(UserMixin): 32 | pass 33 | 34 | 35 | def get_user(user_id: int): 36 | for user in users: 37 | if int(user["id"]) == int(user_id): 38 | return user 39 | 40 | 41 | @login_manager.user_loader 42 | def user_loader(id: int): 43 | user = get_user(id) 44 | if user: 45 | user_model = User() 46 | user_model.id = user["id"] 47 | return user_model 48 | 49 | 50 | @app.route("/api/login", methods=["POST"]) 51 | def login(): 52 | data = request.json 53 | username = data.get("username") 54 | password = data.get("password") 55 | 56 | for user in users: 57 | if user["username"] == username and user["password"] == password: 58 | user_model = User() 59 | user_model.id = user["id"] 60 | login_user(user_model) 61 | return jsonify({"authenticated": True}) 62 | 63 | return jsonify({"authenticated": False}) 64 | 65 | 66 | @app.route("/api/logout", methods=["GET"]) 67 | def logout(): 68 | logout_user() 69 | return jsonify({"authenticated": False}) 70 | 71 | 72 | @app.route("/api/authenticated", methods=["GET"]) 73 | def check_authenticated(): 74 | return jsonify({"authenticated": current_user.is_authenticated}) 75 | 76 | 77 | class SubscribeTopicHandler(client.SubscribeToIoTCoreStreamHandler): 78 | """ 79 | Event handler for SubscribeToTopicOperation 80 | 81 | Inherit from this class and override methods to handle 82 | stream events during a SubscribeToTopicOperation. 83 | """ 84 | 85 | def __init__(self, socket_io_app): 86 | super().__init__() 87 | self.socket_io = socket_io_app 88 | 89 | def on_stream_event(self, event: model.IoTCoreMessage) -> None: 90 | """ 91 | Invoked when a SubscriptionResponseMessage is received. 92 | """ 93 | payload = {"payload": json.loads(event.message.payload.decode())} 94 | 95 | self.socket_io.emit("message", payload) 96 | logger.info("message sent from SubscribeTopicHandler!") 97 | 98 | 99 | def subscribe_to_core(socket_io_app): 100 | ipc_client = awsiot.greengrasscoreipc.connect() 101 | 102 | subscribe_operation = ipc_client.new_subscribe_to_iot_core( 103 | stream_handler=SubscribeTopicHandler(socket_io_app) 104 | ) 105 | subscribe_operation.activate( 106 | request=model.SubscribeToIoTCoreRequest( 107 | topic_name="{}/subscribe".format(os.environ["AWS_IOT_THING_NAME"]), 108 | qos=model.QOS.AT_LEAST_ONCE, 109 | ) 110 | ) 111 | 112 | 113 | @socket_io.on("connect") 114 | def connect_handler(): 115 | if current_user.is_authenticated: 116 | socket_io.emit( 117 | "message", {"payload": "New socket has connected"}, broadcast=True, 118 | ) 119 | logger.info("connect() - authenticated user!") 120 | else: 121 | return False 122 | 123 | 124 | @socket_io.on("publish") 125 | def handle_message(msg): 126 | 127 | if current_user.is_authenticated: 128 | 129 | ipc_client = awsiot.greengrasscoreipc.connect() 130 | 131 | topic = "{}/publish".format(os.environ["AWS_IOT_THING_NAME"]) 132 | data = {"msg": msg} 133 | 134 | publish_operation = ipc_client.new_publish_to_iot_core() 135 | publish_operation.activate( 136 | request=model.PublishToIoTCoreRequest( 137 | topic_name=topic, 138 | qos=model.QOS.AT_MOST_ONCE, 139 | payload=json.dumps(data).encode(), 140 | ) 141 | ) 142 | 143 | 144 | def get_secret(): 145 | 146 | ipc_client = awsiot.greengrasscoreipc.connect() 147 | 148 | get_secret_value = ipc_client.new_get_secret_value() 149 | get_secret_value.activate( 150 | request=model.GetSecretValueRequest(secret_id="localwebserver_credentials") 151 | ) 152 | secret_response = get_secret_value.get_response().result() 153 | secrets = json.loads(secret_response.secret_value.secret_string) 154 | get_secret_value.close() 155 | 156 | users = [ 157 | {"id": 1, "username": secrets["username"], "password": secrets["password"]} 158 | ] 159 | 160 | 161 | if __name__ == "__main__": # pragma: no cover 162 | 163 | get_secret() 164 | subscribe_to_core(socket_io) 165 | socket_io.run(app, host="0.0.0.0", port=5000) 166 | -------------------------------------------------------------------------------- /src/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { io } from "socket.io-client"; 4 | import axios from 'axios'; 5 | 6 | const intervalMilliseconds = 10000; 7 | let endPoint = "https://localhost:3000"; 8 | let socket = io(endPoint, { 9 | autoConnect: false 10 | }); 11 | let intervalId = null; 12 | 13 | 14 | class App extends React.Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | username: '', 20 | password: '', 21 | authenticated: true, 22 | publishString: '', 23 | incomingMessages: '' 24 | }; 25 | 26 | this.publishMessage = this.publishMessage.bind(this); 27 | this.handleInputChange = this.handleInputChange.bind(this); 28 | this.handleSubmit = this.handleSubmit.bind(this); 29 | this.logout = this.logout.bind(this); 30 | } 31 | 32 | handleInputChange(event) { 33 | const target = event.target; 34 | const value = target.value; 35 | const name = target.name; 36 | 37 | this.setState({ 38 | [name]: value 39 | }); 40 | } 41 | 42 | handleSubmit(event) { 43 | 44 | this.login(); 45 | event.preventDefault(); 46 | 47 | } 48 | 49 | componentDidMount() { 50 | 51 | this.check_authentication(); 52 | 53 | } 54 | 55 | publishMessage() { 56 | const { publishString } = this.state; 57 | socket.emit('publish', publishString); 58 | } 59 | 60 | login() { 61 | const { username, password } = this.state; 62 | axios.post(endPoint + '/api/login', { 63 | "username": username, 64 | "password": password 65 | }).then(response => { 66 | this.setState({ authenticated: response.data.authenticated }); 67 | this.start_socket(); 68 | }); 69 | } 70 | 71 | logout() { 72 | 73 | axios.get(endPoint + '/api/logout') 74 | .then(response => { 75 | this.setState({ authenticated: response.data.authenticated }); 76 | this.close_socket(); 77 | }); 78 | } 79 | 80 | close_socket() { 81 | clearInterval(intervalId); 82 | socket.off(); 83 | socket.disconnect(); 84 | } 85 | 86 | start_socket() { 87 | 88 | socket.connect(); 89 | 90 | intervalId = setInterval(() => { 91 | 92 | axios.get(endPoint + '/api/authenticated') 93 | .then(response => { 94 | this.setState({ authenticated: response.data.authenticated }); 95 | const { authenticated } = this.state; 96 | if (!authenticated) { 97 | this.close_socket(); 98 | } 99 | }); 100 | 101 | }, intervalMilliseconds); 102 | 103 | socket.on('connect', () => { 104 | 105 | }); 106 | 107 | socket.on('message', msg => { 108 | console.log(msg); 109 | const date = new Date(); 110 | let dateString = date.toISOString(); 111 | this.setState(prevState => ({ incomingMessages: prevState.incomingMessages.concat(`\n${dateString} - ${JSON.stringify(msg["payload"])}`) })) 112 | }); 113 | 114 | } 115 | 116 | check_authentication() { 117 | 118 | axios.get(endPoint + '/api/authenticated') 119 | .then(response => { 120 | this.setState({ authenticated: response.data.authenticated }); 121 | const { authenticated } = this.state; 122 | if (authenticated) { 123 | this.start_socket(); 124 | } 125 | }); 126 | } 127 | 128 | render() { 129 | const { authenticated } = this.state; 130 | return ( 131 |
132 |
133 | {!authenticated &&
134 |
135 | 136 |
137 |
138 | 139 |
140 |
141 | 142 |
143 |
} 144 | {authenticated && 145 |
146 |
147 | 148 |
149 |
150 |
151 | 152 |
Subscribed to the topic AWS_IOT_THING_NAME/publish
153 |