Merge pull request #21 from corang/master

Vertical flow and new node types
This commit is contained in:
Jordan McClintock 2022-02-24 14:04:16 -06:00 committed by GitHub
commit a68172f2f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 644 additions and 7222 deletions

View File

@ -23,8 +23,8 @@
"category": "",
"in": [
{
"x": 240,
"y": 180,
"x": 140,
"y": 40,
"wires": [
{
"id": "73521b98841c6355"
@ -34,8 +34,8 @@
],
"out": [
{
"x": 1720,
"y": 300,
"x": 550,
"y": 630,
"wires": [
{
"id": "752d2d1fc700d27e",
@ -62,12 +62,12 @@
},
{
"id": "73521b98841c6355",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Kubernetes",
"info": "# Description\n\n\n## Resources\n- https://kubernetes.io/docs/home/ ***\n- https://www.youtube.com/watch?v=VnvRFRk_51k \n- https://www.youtube.com/watch?v=X48VuDVv0do (long video, covers most topics in this flow)",
"x": 450,
"y": 180,
"x": 240,
"y": 130,
"wires": [
[
"90665185044d3fc2",
@ -79,11 +79,11 @@
},
{
"id": "90665185044d3fc2",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Kubernetes Objects",
"info": "",
"x": 780,
"x": 700,
"y": 180,
"wires": [
[
@ -94,48 +94,48 @@
},
{
"id": "ccf8bad6e9ebfb6a",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Kubernetes Networking",
"info": "",
"x": 810,
"y": 260,
"x": 440,
"y": 320,
"wires": [
[]
]
},
{
"id": "4146a7d5608cc03b",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Kubernetes Storage",
"info": "",
"x": 820,
"y": 340,
"x": 460,
"y": 220,
"wires": [
[]
]
},
{
"id": "418551f904d62b20",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Kubernetes App Manifests",
"info": "",
"x": 780,
"y": 420,
"x": 200,
"y": 270,
"wires": [
[]
]
},
{
"id": "0d309e7b063a4ce2",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Configurations",
"info": "",
"x": 1040,
"y": 220,
"x": 650,
"y": 270,
"wires": [
[
"c53965f3ddc770ad",
@ -145,12 +145,12 @@
},
{
"id": "d810b83f64f0bc5e",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Deployments",
"info": "",
"x": 1030,
"y": 100,
"x": 820,
"y": 270,
"wires": [
[
"752d2d1fc700d27e"
@ -159,48 +159,48 @@
},
{
"id": "752d2d1fc700d27e",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Pods",
"info": "",
"x": 1290,
"y": 100,
"x": 810,
"y": 420,
"wires": [
[]
]
},
{
"id": "c53965f3ddc770ad",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Configmaps",
"info": "",
"x": 1270,
"y": 200,
"x": 530,
"y": 420,
"wires": [
[]
]
},
{
"id": "e902145181ebd724",
"type": "comment",
"type": "resource",
"z": "0ac7681ed1c2bc4b",
"name": "Secrets",
"info": "",
"x": 1260,
"y": 280,
"x": 680,
"y": 420,
"wires": [
[]
]
},
{
"id": "5dfd2ba38ec5178d",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Containerization",
"info": "",
"x": 80,
"y": 300,
"x": 430,
"y": 50,
"wires": [
[
"ea946e25b7c2079f"
@ -209,12 +209,12 @@
},
{
"id": "8c04bec4a15f6702",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Helm",
"info": "",
"x": 890,
"y": 440,
"x": 610,
"y": 200,
"wires": [
[
"e328f5849309e731"
@ -223,12 +223,12 @@
},
{
"id": "fc01372231b580db",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Custom Resource Definitions",
"info": "",
"x": 940,
"y": 500,
"x": 430,
"y": 200,
"wires": [
[
"e328f5849309e731"
@ -237,12 +237,12 @@
},
{
"id": "09d8e90d5b4ade12",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Kustomization",
"info": "",
"x": 920,
"y": 640,
"x": 90,
"y": 200,
"wires": [
[
"e328f5849309e731"
@ -251,12 +251,12 @@
},
{
"id": "71a96ec4058930db",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "GitOps",
"info": "",
"x": 920,
"y": 260,
"x": 720,
"y": 150,
"wires": [
[
"45465f711df6da7e"
@ -265,12 +265,12 @@
},
{
"id": "98df2d11adb09046",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "git",
"info": "# Description\n\n[Git](https://git-scm.com/) is a free and open source distributed version control system.\n\n## Resources\n- https://git-scm.com/docs\n\n\n## Examples\n- github.com\n- gitlab.com\n- https://gitea.com/",
"x": 750,
"y": 180,
"x": 720,
"y": 50,
"wires": [
[
"71a96ec4058930db"
@ -279,12 +279,12 @@
},
{
"id": "45465f711df6da7e",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Flux",
"info": "",
"x": 1130,
"y": 300,
"x": 720,
"y": 200,
"wires": [
[
"e328f5849309e731"
@ -293,12 +293,12 @@
},
{
"id": "e328f5849309e731",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Big Bang",
"info": "",
"x": 1440,
"y": 480,
"x": 430,
"y": 620,
"wires": [
[]
]
@ -308,8 +308,8 @@
"type": "subflow:0ac7681ed1c2bc4b",
"z": "eb7909bcf5fcaf8a",
"name": "",
"x": 400,
"y": 300,
"x": 430,
"y": 110,
"wires": [
[
"8c04bec4a15f6702",
@ -322,12 +322,12 @@
},
{
"id": "81348cdecdf51fd9",
"type": "comment",
"type": "resource",
"z": "eb7909bcf5fcaf8a",
"name": "Operators",
"info": "https://www.youtube.com/watch?v=ha3LjlD6g7g",
"x": 890,
"y": 380,
"x": 240,
"y": 200,
"wires": [
[
"e328f5849309e731"
@ -336,26 +336,28 @@
},
{
"id": "40bfe328130d9c41",
"type": "comment",
"type": "guide",
"z": "c86af370eff94afa",
"name": "Deploy K3d",
"info": "https://k3d.io/v5.2.2/\n\n### Choose your platform\nThe preferred free VM software is [VirtualBox](https://www.virtualbox.org/); however, it is not compatible with M1 chips! So, if you are using an M1 Mac, we highly recommend you go with the [Cloud](#Cloud) option below\n\n##### Cloud\nRecommend using an EC2 instance with the following specs:\n- AMI: Ubuntu Server (64-bit, x86)\n- Instance Type: t3a.2xlarge\n- 100 GB EBS\n- Security group rules for allowing web traffic\n\n##### Local\nUse [Vagrant](https://www.vagrantup.com/) to spin up a VM with at least the following specs:\n- 10GB RAM\n- 4 CPU cores\n- An IP that can be [accessed](https://www.vagrantup.com/docs/networking/private_network#static-ip) from the host machine. \n\nDepending on RAM/CPU availability, you may want to run 2 or more K3d nodes\n\n## Cluster creation\n\nOnce you have a host for deploying a k3d cluster - you can configure a cluster for future nodes in this training.\n\nOne possible configuration would be a 3 node cluster with 1 server and 2 agents (to get started).\n\nAnother consideration is exposing applications after the cluster is running and apps have been deployed w/ services and ingresses.\n\nA possible configuration might look like:\n\n`k3d cluster create -s 1 -a 2 -p \"8081:80@loadbalancer\" dev-cluster`\n\n-s 1 represents 1 server node\n-a 2 represents 2 agent nodes\n\n-p \"8081:80@loadbalancer\" represents mapping 8081 on this k3d host to ports 80 internally.",
"x": 240,
"y": 160,
"info": "Throughout this guide you will be using K3d to deploy and manage kubernetes instances.\n\n## Choose your platform\nThe preferred free VM software is [VirtualBox](https://www.virtualbox.org/); however, it is not compatible with M1 chips! So, if you are using an M1 Mac, we highly recommend you go with the [Cloud](#Cloud) option below\n\n### Cloud\nRecommend using an EC2 instance with the following specs:\n- AMI: Ubuntu Server (64-bit, x86)\n- Instance Type: t3a.2xlarge\n- 100 GB EBS\n- Security group rules for allowing web traffic\n\n### Local VM\nUse [Vagrant](https://www.vagrantup.com/) to spin up a VM with at least the following specs:\n- 10GB RAM\n- 4 CPU cores\n- An IP that can be [accessed](https://www.vagrantup.com/docs/networking/private_network#static-ip) from the host machine. \n\n### Local\nAny Docker installation except a non-WSL2 Windows installation will work with K3d natively.\n\n## K3d setup\n\nSetup instructions for K3d can be found at the link below.\n\nhttps://k3d.io/v5.2.2/\n\n## Cluster creation\n\nOnce you have a host for deploying a k3d cluster - you can configure a cluster for future nodes in this training.\n\nOne possible configuration would be a 3 node cluster with 1 server and 2 agents (to get started).\n\nAnother consideration is exposing applications after the cluster is running and apps have been deployed w/ services and ingresses.\n\nDepending on RAM/CPU availability, you may want to run 2 or more K3d nodes\n\nA possible configuration might look like:\n\n`k3d cluster create -s 1 -a 2 -p \"8081:80@loadbalancer\" dev-cluster`\n\n-s 1 represents 1 server node\n-a 2 represents 2 agent nodes\n\n-p \"8081:80@loadbalancer\" represents mapping 8081 on this k3d host to ports 80 internally.",
"x": 360,
"y": 60,
"wires": [
[
"691424f6cfd58372"
"691424f6cfd58372",
"1458b18889c13942",
"6d2d525b92151db2"
]
]
},
{
"id": "691424f6cfd58372",
"type": "comment",
"type": "guide",
"z": "c86af370eff94afa",
"name": "Deploy a pod info application (with Kustomize)",
"info": "# Kustomize\n\n\"Kustomize is a command-line configuration manager for Kubernetes objects. Integrated with kubectl since 1.14, it allows you to make declarative changes to your configurations without touching a template.\"\n\n## Recommended Reading\n\n`https://www.mirantis.com/blog/introduction-to-kustomize-part-1-creating-a-kubernetes-app-out-of-multiple-pieces/`\n\nThe above tutorial can be run on the k3d cluster you have created. This is a much more complex example than the podinfo example you will deploy below.\n\n## Podinfo\n\n\"Podinfo is a tiny web application made with Go that showcases best practices of running microservices in Kubernetes\"\n\n### Deployment\n\nNavigate to `https://github.com/stefanprodan/podinfo`\n\nClone the repository to your local machine\n\nFrom within the cloned directory, we can execute kustomize through the built in functionality in `kubectl`.\n\n`kubectl kustomize ./kustomize`\n\nThis will return the built manifest for the application to be deployed. You can then deploy the application to the cluster through `kubectl apply -k ./kustomize`\n\n## Success Criteria\n\n- 2 podinfo pods in the target namespace (default if not specified)\n- A podinfo service\n- A horizontal pod autoscaler\n\nWe can port-forward this applications service and visit it in browser to confirm functionality.\n\n`kubectl port-forward service/podinfo 9898:http`\n\nThis port fowards the podinfo service `http` port (as defined in the service) to the 9898 host port.",
"x": 580,
"y": 160,
"x": 420,
"y": 190,
"wires": [
[
"2d66fece0a8f5fdf"
@ -364,12 +366,12 @@
},
{
"id": "2d66fece0a8f5fdf",
"type": "comment",
"type": "task",
"z": "c86af370eff94afa",
"name": "Expose podinfo with an Ingress",
"info": "# Ingress\n\nIn the previous exercise, we deployed the podinfo application and confirmed functionality by visiting it in-browser through port-forwarding with kubectl. We can instead use an ingress and the default traefik ingress-controller to handle this functionaity more natively. \n\nOfficial k3d docs: https://k3d.io/v5.0.0/usage/exposing_services/\n\n## Ingress deployment\nDue to the cluster configuration that we executed in the first node (See the -p loadbalancer parameter). we can configure an ingress to expose the application, as is one of a few standard practices for exposing internal applications to external traffic.\n\n### Ingress template\n\nIngress docs: https://kubernetes.io/docs/concepts/services-networking/ingress/\n\nGiven the template from the docs/tutorial we can write an ingress to support this traffic.\n\n## Success Criteria\n\nAfter the ingress is deployed (and given you configured your cluster as described in the first node), then you should be able to access the podinfo application without port-forwarding at `http://localhost:8081` \n\n## Cleanup\n\nThis concludes this deployment of podinfo. You'll want to cleanup the resources we have created. ",
"x": 1010,
"y": 160,
"x": 420,
"y": 260,
"wires": [
[
"52eb692e4c51108f"
@ -378,12 +380,12 @@
},
{
"id": "52eb692e4c51108f",
"type": "comment",
"type": "guide",
"z": "c86af370eff94afa",
"name": "Create a podinfo helm chart",
"info": "# Helm\n\"Helm is the package manager for Kubernetes\"\n\n## Recommended Reading\nhttps://helm.sh/\nhttps://helm.sh/docs/intro/\n\n## Podinfo Helm Chart from Scratch\n\nTODO - Insert content for:\n- deployment\n- service\n- ingress\n- HPA\n* All templated from scratch\n\n## Deploy the official podinfo chart from local files\n\nPreviously we cloned the podinfo repository to our/a local machine. Under the root of the project there is a `charts` directory with a `podinfo` directory that contains the podinfo chart content.\n\n## Basic deployment\n\nLet's create a testing namespace for our target\n`kubectl create ns testing`\n\nWith Helm installed and our k3d cluster still running/configured, we can install the chart in it's vanilla form (without enabling any additional content).\n\n(From the charts directory)\n`helm install podinfo-dev ./podinfo -n testing`\n\nThis will deploy the chart which results in the deployment and service creation in the target namespace.\n\n## Customizations\nWe can inject exposed customizations as outlined in the README/values.yaml for the purpose of configuring the end package being suited for our needs.\n\nWe can make an edit to the values.yaml and upgrade our application.\n\nhpa:\n enabled: true\n \n`helm upgrade podinfo-dev -n testing ./podinfo`\n\nThis should result in an HPA being deployed to our namespace for the application.\n",
"x": 1410,
"y": 160,
"x": 420,
"y": 330,
"wires": [
[
"5361df412a99db12"
@ -392,12 +394,12 @@
},
{
"id": "5361df412a99db12",
"type": "comment",
"type": "guide",
"z": "c86af370eff94afa",
"name": "Deploy BigBang on a new K3d cluster",
"info": "# Big Bang\n\nUsing our machine with k3d, we can instantiate a development/prototype deployment of Big Bang.\n\n## Objective\n\nFollow the qiuckstart here:\nhttps://repo1.dso.mil/platform-one/big-bang/bigbang/-/blob/master/docs/guides/deployment_scenarios/quickstart.md\n\n## Success Criteria\n\nAs noted in step 13 - the web UI's should be resolvable and accessible. All pods should be up and healthy (running). ",
"x": 1810,
"y": 160,
"x": 420,
"y": 400,
"wires": [
[
"67415f95eb8ab46c"
@ -406,12 +408,12 @@
},
{
"id": "67415f95eb8ab46c",
"type": "comment",
"type": "task",
"z": "c86af370eff94afa",
"name": "Deploy podinfo helmchart in a bigbang cluster",
"info": "# Big Bang Extensibility\n\nUp to now - we have configured and deployed k3d clusters, we've used `kustomize` and `Helm` to orchestrate deploying `podinfo` to our cluster.\n\nWe then used flux and it's controllers to deploy bigbang (which is using helm and kustomize under the hood).\n\n## Recommended Reading\n\n## Objective\nNow we will look at extending the big bang deployment to include deploying the podinfo helm chart and ensuring the deployment architecture aligns with the Big Bang \"Core\" technologies.\n\nThis is a precursory step to follow-on nodes in this flow. \n\n## Execution\n\nClone the podinfo repository if not done already.\n\nCreate a namespace with the label:\n`istio-injection: enabled`\n\nDeploy the helm chart for the podinfo applicaiton as we have done previously.\n\n## Success Criteria\nDeploy the podinfo helm chart into your cluster that already has big bang deployed.\n\nThe application should come online and be able to be reached via `kubectl port-forward`\n\nContinue to the next node for extending this with Istio",
"x": 2270,
"y": 160,
"x": 420,
"y": 470,
"wires": [
[
"9b8fdffdc9b5cc53"
@ -420,12 +422,12 @@
},
{
"id": "9b8fdffdc9b5cc53",
"type": "comment",
"type": "task",
"z": "c86af370eff94afa",
"name": "Add Istio Virtual Service for the podinfo deployment",
"info": "# Istio VirtualServices\n\n## Recommended Reading\n\n- https://istio.io/latest/docs/reference/config/networking/virtual-service/\n\n### Example\n- https://repo1.dso.mil/platform-one/big-bang/apps/core/kiali/-/blob/main/chart/values.yaml#:~:text=istio%3A,kiali.%7B%7B%20.Values.hostname%20%7D%7D\n- https://repo1.dso.mil/platform-one/big-bang/apps/core/kiali/-/blob/main/chart/templates/bigbang/virtualservice.yaml\n\n## Objective\nWith the podinfo helm chart deployed (in it's basic state), we will look to leverage some of th technologies that Big Bang has reconciled and configured.\n\nBig Bang exposes cluster applications to external traffic through an Istio Ingress Gateway.\n\nVirtual services are then used to link kubernetes services to traffic from the ingresss gateways.\n\n## Execution\n\n\n\n## Success Criteria\nWrite a virtual service resource definition file for the podinfo application that was deployed as part of the last node.\n\nIf you add the `podinfo.bigbang.dev` endpoint to your hosts file, then it should be resolveable from the browser.\n\n## Solution\n<details><summary>show</summary>\n<p>\n\n```\napiVersion: networking.istio.io/v1beta1\nkind: VirtualService\nmetadata:\n name: podinfo\n namespace: podinfo\nspec:\n gateways:\n - istio-system/public\n hosts:\n - podinfo.bigbang.dev\n http:\n - route:\n - destination:\n host: podinfo.podinfo.svc.cluster.local\n port:\n number: 9898\n```\n\n</p>\n</details>",
"x": 2750,
"y": 160,
"x": 420,
"y": 540,
"wires": [
[
"339481fd79e107a6"
@ -434,12 +436,12 @@
},
{
"id": "339481fd79e107a6",
"type": "comment",
"type": "task",
"z": "c86af370eff94afa",
"name": "Deploy podinfo as a Flux HelmRelease",
"info": "# Flux helm release reconciliation\n\"Big Bang follows a GitOps approach to configuration management, using Flux v2 to reconcile Git with the cluster. Environments (e.g. dev, prod) and packages (e.g. istio) can be fully configured to suit the deployment needs.\"\n\n## Prerequisites\n- Remove all prior content for podinfo from your cluster. \n\n## Recommended Reading\n- https://repo1.dso.mil/platform-one/big-bang/bigbang/-/blob/master/README.md\n\n## Objective\nFollowing the umbrella structure in the Big Bang chart, we can create the templates under a `podinfo` directory (under the templates dir).\n\nNote: there are many ways to accomplish this task - the podinfo directory under the templates directory most-accurately represents how the umbrella structure works.\n\n### Minimum Requirements\n- namespace resource\n- git repository resource\n- helm release resource\n- virtual service resource\n\nThis would meet the bare minimum requirements.\n\n### Additional Content\n- values file (a helper generates a secret resource from this)\n\n## Success Criteria\nIf we run the `helm upgrade` command against the modified Big Bang helm chart, it should deploy the podinfo chart (if properly enabled) with the ability to access/resolve the application through the `Ingress Gateway`.",
"x": 3210,
"y": 160,
"x": 420,
"y": 610,
"wires": [
[
"0f9889d8e9cb7edb"
@ -448,14 +450,110 @@
},
{
"id": "0f9889d8e9cb7edb",
"type": "comment",
"type": "guide",
"z": "c86af370eff94afa",
"name": "Play DOOM using ZARF",
"info": "# Zarf!\n\nZarf is a Defense Unicorns developed tool - please read about it below!\n\n## Recommended Reading\n- https://github.com/defenseunicorns/zarf\n\n\n## Objective\nUsing Zarf, we will follow the guide below in order to play DOOM.\n\nhttps://github.com/defenseunicorns/zarf/tree/master/examples/game\n\n## Success Criteria\nPackaging, Deployment and execution of playing DOOM from within the browser.",
"x": 3620,
"y": 160,
"x": 420,
"y": 680,
"wires": [
[]
]
},
{
"id": "1458b18889c13942",
"type": "guide",
"z": "c86af370eff94afa",
"name": "Deploy and Access the Kubernetes Dashboard",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 800,
"y": 190,
"wires": [
[]
]
},
{
"id": "6d2d525b92151db2",
"type": "resource",
"z": "c86af370eff94afa",
"name": "Kustomize",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 230,
"y": 120,
"wires": [
[
"691424f6cfd58372"
]
]
},
{
"id": "a25127e60aa3afac",
"type": "resource",
"z": "c86af370eff94afa",
"name": "Helm",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 150,
"y": 260,
"wires": [
[
"52eb692e4c51108f"
]
]
},
{
"id": "bd30d9f248bb244a",
"type": "resource",
"z": "c86af370eff94afa",
"name": "Big Bang",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 150,
"y": 330,
"wires": [
[
"5361df412a99db12"
]
]
},
{
"id": "f17eae05b57f6a2e",
"type": "resource",
"z": "c86af370eff94afa",
"name": "Istio",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 150,
"y": 470,
"wires": [
[
"9b8fdffdc9b5cc53"
]
]
},
{
"id": "3a185c296c95de08",
"type": "resource",
"z": "c86af370eff94afa",
"name": "Flux",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 150,
"y": 540,
"wires": [
[
"339481fd79e107a6"
]
]
},
{
"id": "14c8378318de0a59",
"type": "resource",
"z": "c86af370eff94afa",
"name": "Traefix Ingress",
"info": "# Description\n\n# Resources\n\n# Unicorn SME(s)\n",
"x": 150,
"y": 190,
"wires": [
[
"2d66fece0a8f5fdf"
]
]
}
]

View File

@ -2,7 +2,7 @@
Sparkles guide is forked from Node-red, which has a LOT of functionality in addition to the nice UI we use. As such, much code is not relevant for our purpose.
Since this is a derivative work, using an Apache 2.0 license. We MUST add a modification copyright to any changed files not originated by us. Add `2021 Modification Copyright - Defense Unicorns` as a comment to the top of any changed file.
Since this is a derivative work, using an Apache 2.0 license. We MUST add a modification copyright to any changed files not originated by us. Add `2022 Modification Copyright - Defense Unicorns` as a comment to the top of any changed file.
Finally, deleting code often has unintended consequences - you should, at a minimum, add cypress tests to confirm basic, common functionality to ensure functionality.

View File

@ -11,7 +11,7 @@ At the simplest level:
## Conventions and Guidelines
- Topics within flows, build upon each other from left to right. For example, knowing containerization prior to kubernetes.
- Topics within flows, build upon each other from top to bottom. For example, knowing containerization prior to kubernetes.
- Use subflows when a common topic exists. Ex. Zarf requires golang, which is a learning topic itself, or BigBang requires kubernetes
- Add materials (youtube, course, SME, article, etc...) to the description of nodes.
- Add a * to materials that you found helpful

1921
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
{
"_comment" : "2022 Modification Copyright - Defense Unicorns",
"common": {
"label": {
"name": "Name",
@ -86,7 +87,8 @@
"rtl": "Right-to-left",
"auto": "Contextual",
"language": "Language",
"browserDefault": "Browser default"
"browserDefault": "Browser default",
"flowVertical": "Vertical flow"
},
"sidebar": {
"show": "Show sidebar"

View File

@ -14,6 +14,8 @@
* limitations under the License.
**/
// 2022 Modification Copyright - Defense Unicorns
RED.palette = (function() {
var exclusion = ['config','unknown','deprecated'];
@ -147,7 +149,14 @@ RED.palette = (function() {
var labelElement = el.find(".red-ui-palette-label");
labelElement.html(lines).attr('dir', RED.text.bidi.resolveBaseTextDir(lines));
el.find(".red-ui-palette-port").css({top:(multiLineNodeHeight/2-5)+"px"});
if (RED.view.vertical()) {
el.find(".red-ui-palette-port").css({top:(multiLineNodeHeight/2-5)+"px", left: "55px"});
el.find(".red-ui-palette-port-output").css({top:(multiLineNodeHeight-5)+"px", right: "55px"});
el.find(".red-ui-palette-port-input").css({top: "-5px"});
}else{
el.find(".red-ui-palette-port").css({top:(multiLineNodeHeight/2-5)+"px"});
// el.find(".palette_port_output").css({top:(multiLineNodeHeight/2-5)+"px", right: "-6px", left: "auto"});
}
var popOverContent;
try {

View File

@ -14,6 +14,8 @@
* limitations under the License.
**/
// 2022 Modification Copyright - Defense Unicorns
RED.userSettings = (function() {
var trayWidth = 700;
@ -133,7 +135,8 @@ RED.userSettings = (function() {
options: [
{setting:"view-show-grid",oldSetting:"menu-menu-item-view-show-grid",label:"menu.label.view.showGrid", default: true, toggle:true,onchange:"core:toggle-show-grid"},
{setting:"view-snap-grid",oldSetting:"menu-menu-item-view-snap-grid",label:"menu.label.view.snapGrid", default: true, toggle:true,onchange:"core:toggle-snap-grid"},
{setting:"view-grid-size",label:"menu.label.view.gridSize",type:"number",default: 20, onchange:RED.view.gridSize}
{setting:"view-grid-size",label:"menu.label.view.gridSize",type:"number",default: 10, onchange:RED.view.gridSize},
{setting:"view-flow-vertical",label:"menu.label.view.flowVertical",toggle:true,default:true,onchange:RED.view.vertical}
]
},
{

View File

@ -14,6 +14,7 @@
* limitations under the License.
**/
// 2022 Modification Copyright - Defense Unicorns
/* <div>#red-ui-workspace-chart
* \- <svg> "outer"
@ -28,7 +29,7 @@
* |- <g> "nodeLayer"
*/
RED.view = (function() {
RED.view = (function() {
var space_width = 5000,
space_height = 5000,
lineCurveScale = 0.75,
@ -47,6 +48,7 @@ RED.view = (function() {
var gridSize = 20;
var snapGrid = false;
var vertical = false;
var activeSpliceLink;
var spliceActive = false;
@ -927,6 +929,75 @@ RED.view = (function() {
}
}
function generateLinkPathV(origX,origY, destX, destY, sc) {
var dy = destY-origY;
var dx = destX-origX;
var delta = Math.sqrt(dy*dy+dx*dx);
var scale = lineCurveScale;
var scaleX = 0;
if (dy*sc > 0) {
if (delta < node_height) {
scale = 0.75-0.75*((node_height-delta)/node_height);
// scale += 2*(Math.min(5*node_height,Math.abs(dx))/(5*node_height));
// if (Math.abs(dy) < 3*node_height) {
// scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_height,Math.abs(dx))/(node_height)) ;
// }
}
} else {
scale = 0.4-0.2*(Math.max(0,(node_height-Math.min(Math.abs(dx),Math.abs(dy)))/node_height));
}
if (dy*sc > 0) {
return "M "+origX+" "+origY+
" C "+(origX+(node_width*scaleX))+" "+(origY+sc*scale*node_height)+" "+
(destX-(node_width*scaleX))+" "+(destY-sc*scale*node_height)+" "+
destX+" "+destY
} else {
var midX = Math.floor(destX-dx/2);
var midY = Math.floor(destY-dy/2);
//
if (dx === 0) {
midX = destX + node_width;
}
var cp_width = node_width/2;
var x1 = (destX + midX)/2;
var topX = dx>0? Math.min(x1 - dx/2 , origX+cp_width) : Math.max(x1 - dx/2 , origX-cp_width);
var topY = origY + sc*node_height*scale;
var bottomX = dx>0?Math.max(x1, destX-cp_width):Math.min(x1, destX+cp_width);
var bottomY = destY - sc*node_height*scale;
var y1 = (origY+topY)/2;
var scx = dx>0?1:-1;
var cp = [
[origX,y1],
[dx>0 ? Math.max(origX, topX-cp_width) : Math.min(origX, topX+cp_width), topY],
[dx>0 ? Math.max(midX, topX-cp_width) : Math.min(midX, topX-cp_width), y1],
[dx>0 ? Math.max(midX, bottomX-cp_width): Math.min(midX, bottomX+cp_width), bottomY],
[destX,(destY+bottomY)/2]
];
if (cp[2][0] === topX+scx*cp_width) {
if (Math.abs(dx) < cp_width*10) {
cp[1][0] = topX-scx*cp_width/2;
cp[3][0] = bottomX-scx*cp_width/2;
}
cp[2][1] = topY;
}
return "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
topX+" "+topY+
" S "+
cp[2][0]+" "+cp[2][1]+" "+
midX+" "+midY+
" S "+
cp[3][0]+" "+cp[3][1]+" "+
bottomX+" "+bottomY+
" S "+
cp[4][0]+" "+cp[4][1]+" "+
destX+" "+destY
}
}
function addNode(type,x,y) {
var m = /^subflow:(.+)$/.exec(type);
@ -976,8 +1047,13 @@ RED.view = (function() {
nn.changed = true;
nn.moved = true;
nn.w = node_width;
nn.h = Math.max(node_height,(nn.outputs||0) * 15);
if (vertical) {
nn.w = Math.max(node_width,(nn.outputs||0) * 15);
nn.h = node_height;
} else {
nn.w = node_width;
nn.h = Math.max(node_height,(nn.outputs||0) * 15);
}
nn.resize = true;
var historyEvent = {
@ -1143,9 +1219,17 @@ RED.view = (function() {
}
var numOutputs = (quickAddLink.portType === PORT_TYPE_OUTPUT)?(quickAddLink.node.outputs || 1):1;
var sourcePort = quickAddLink.port;
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
if (vertical) {
var portX = -((numOutputs-1)/2)*13 +13*sourcePort;
} else {
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
}
var sc = (quickAddLink.portType === PORT_TYPE_OUTPUT)?1:-1;
quickAddLink.el.attr("d",generateLinkPath(quickAddLink.node.x+sc*quickAddLink.node.w/2,quickAddLink.node.y+portY,point[0]-sc*node_width/2,point[1],sc));
if (vertical) {
quickAddLink.el.attr("d",generateLinkPathV(quickAddLink.node.x+portX,quickAddLink.node.y+sc*quickAddLink.node.h/2,point[0]-sc*node_width/2,point[1],sc));
}else{
quickAddLink.el.attr("d",generateLinkPath(quickAddLink.node.x+sc*quickAddLink.node.w/2,quickAddLink.node.y+portY,point[0]-sc*node_width/2,point[1],sc));
}
}
if (quickAddLink) {
rebuildQuickAddLink();
@ -1554,10 +1638,18 @@ RED.view = (function() {
var drag_line = drag_lines[i];
var numOutputs = (drag_line.portType === PORT_TYPE_OUTPUT)?(drag_line.node.outputs || 1):1;
var sourcePort = drag_line.port;
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
if (vertical) {
var portX = -((numOutputs-1)/2)*13 +13*sourcePort;
} else {
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
}
var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1;
drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc));
if (vertical) {
drag_line.el.attr("d",generateLinkPathV(drag_line.node.x+portX,drag_line.node.y+sc*drag_line.node.h/2,mousePos[0],mousePos[1],sc));
} else {
drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc));
}
}
d3.event.preventDefault();
} else if (mouse_mode == RED.state.MOVING) {
@ -4051,8 +4143,9 @@ RED.view = (function() {
node[0][0].__textGroup__ = text;
nodeContents.append(text);
var attr_output_translate = (vertical) ? 'translate(15,-5)' : 'translate(-5,15)';
var portEl = document.createElementNS("http://www.w3.org/2000/svg","g");
portEl.setAttribute('transform','translate(-5,15)')
portEl.setAttribute('transform',attr_output_translate)
var port = document.createElementNS("http://www.w3.org/2000/svg","rect");
port.setAttribute("class","red-ui-flow-port");
@ -4090,7 +4183,8 @@ RED.view = (function() {
.on("touchstart",nodeTouchStart)
.on("touchend", nodeTouchEnd);
inGroup.append("g").attr('transform','translate(35,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
var attr_input_translate = (vertical) ? 'translate(15,35)' : 'translate(35,15)';
inGroup.append("g").attr('transform',attr_input_translate).append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);} )
.on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);d3.event.preventDefault();} )
.on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);})
@ -4115,7 +4209,8 @@ RED.view = (function() {
.on("touchstart",nodeTouchStart)
.on("touchend", nodeTouchEnd);
statusGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
var attr_status_translate = (vertical) ? 'translate(15,-5)' : 'translate(-5,15)';
statusGroup.append("g").attr('transform',attr_status_translate).append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
.on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
.on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);})
@ -4432,7 +4527,11 @@ RED.view = (function() {
if (hideLabel) {
d.w = node_height;
} else {
d.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(d._def.inputs>0?7:0))/20)) );
if (vertical) {
d.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(d._def.inputs>0?7:0))/20)),(d.outputs||0) * 15 );
}else{
d.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(d._def.inputs>0?7:0))/20)) );
}
}
if (ow !== undefined) {
d.x += (d.w-ow)/2;
@ -4527,7 +4626,7 @@ RED.view = (function() {
if (d.type === "link in") {
inputGroupPorts = inputGroup.append("circle")
.attr("cx",-1).attr("cy",5)
.attr("cx",vertical ? 5 : -1).attr("cy",vertical ? -1 : 5)
.attr("r",5)
.attr("class","red-ui-flow-port red-ui-flow-link-port")
} else {
@ -4553,7 +4652,11 @@ RED.view = (function() {
numOutputs = 0;
}
}
var y = (d.h/2)-((numOutputs-1)/2)*13;
if (vertical) {
var x = (d.w/2)-((numOutputs-1)/2)*13;
}else{
var y = (d.h/2)-((numOutputs-1)/2)*13;
}
// Remove extra ports
while (this.__outputs__.length > numOutputs) {
@ -4575,8 +4678,8 @@ RED.view = (function() {
var portPort;
if (d.type === "link out") {
portPort = document.createElementNS("http://www.w3.org/2000/svg","circle");
portPort.setAttribute("cx",11);
portPort.setAttribute("cy",5);
portPort.setAttribute("cx",vertical ? 6 : 11);
portPort.setAttribute("cy",vertical ? 11 : 6);
portPort.setAttribute("r",5);
portPort.setAttribute("class","red-ui-flow-port red-ui-flow-link-port");
} else {
@ -4605,9 +4708,18 @@ RED.view = (function() {
} else {
portGroup = this.__outputs__[portIndex];
}
var x = d.w - 5;
var y = (d.h/2)-((numOutputs-1)/2)*13;
portGroup.setAttribute("transform","translate("+x+","+((y+13*portIndex)-5)+")")
if (vertical) {
x = (d.w/2)-((numOutputs-1)/2)*13;
var y = d.h - 5;
}else{
y = (d.h/2)-((numOutputs-1)/2)*13;
var x = d.w - 5;
}
if (vertical) {
portGroup.setAttribute("transform","translate("+((d.w/2)-5)+", 25)")
}else{
portGroup.setAttribute("transform","translate("+x+","+((y+13*i)-5)+")")
}
}
if (d._def.icon) {
var icon = thisNode.select(".red-ui-flow-node-icon");
@ -4645,7 +4757,11 @@ RED.view = (function() {
thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) {
var port = d3.select(this);
port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";})
if (vertical) {
port.attr("transform",function(d){return "translate("+((d.w/2)-5)+", -5)";})
}else{
port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";})
}
});
if (d._def.button) {
@ -4765,17 +4881,23 @@ RED.view = (function() {
if (d.added || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) {
var numOutputs = d.source.outputs || 1;
var sourcePort = d.sourcePort || 0;
var y = -((numOutputs-1)/2)*13 +13*sourcePort;
d.x1 = d.source.x+d.source.w/2;
d.y1 = d.source.y+y;
d.x2 = d.target.x-d.target.w/2;
d.y2 = d.target.y;
var path;
if (vertical) {
var x = -((numOutputs-1)/2)*13 +13*sourcePort;
d.x1 = d.source.x+x;
d.y1 = d.source.y+d.source.h/2;
d.x2 = d.target.x;
d.y2 = d.target.y-d.target.h/2;
path = generateLinkPathV(d.x1,d.y1,d.x2,d.y2,1);
} else {
var y = -((numOutputs-1)/2)*13 +13*sourcePort;
d.x1 = d.source.x+d.source.w/2;
d.y1 = d.source.y+y;
d.x2 = d.target.x-d.target.w/2;
d.y2 = d.target.y;
path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
}
// return "M "+d.x1+" "+d.y1+
// " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
// (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
// d.x2+" "+d.y2;
var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
if (/NaN/.test(path)) {
path = ""
}
@ -5664,6 +5786,14 @@ RED.view = (function() {
updateGrid();
}
},
vertical: function(v) {
if (v === undefined) {
return vertical;
} else {
vertical = v;
RED.view.redraw();
}
},
getActiveNodes: function() {
return activeNodes;
},

View File

@ -1,711 +0,0 @@
<!--
Copyright JS Foundation and other contributors, http://js.foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/html" data-template-name="inject">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row node-input-property-container-row">
<ol id="node-input-property-container"></ol>
</div>
<div class="form-row" id="node-once">
<label for="node-input-once">&nbsp;</label>
<input type="checkbox" id="node-input-once" style="display:inline-block; width:15px; vertical-align:baseline;">
<span data-i18n="inject.onstart"></span>&nbsp;
<input type="text" id="node-input-onceDelay" placeholder="0.1" style="width:45px; height:28px;">&nbsp;
<span data-i18n="inject.onceDelay"></span>
</div>
<div class="form-row">
<label for=""><i class="fa fa-repeat"></i> <span data-i18n="inject.label.repeat"></span></label>
<select id="inject-time-type-select">
<option value="none" data-i18n="inject.none"></option>
<option value="interval" data-i18n="inject.interval"></option>
<option value="interval-time" data-i18n="inject.interval-time"></option>
<option value="time" data-i18n="inject.time"></option>
</select>
<input type="hidden" id="node-input-repeat">
<input type="hidden" id="node-input-crontab">
</div>
<div class="form-row inject-time-row hidden" id="inject-time-row-interval">
<span data-i18n="inject.every"></span>
<input id="inject-time-interval-count" class="inject-time-count" value="1"></input>
<select style="width:100px" id="inject-time-interval-units">
<option value="s" data-i18n="inject.seconds"></option>
<option value="m" data-i18n="inject.minutes"></option>
<option value="h" data-i18n="inject.hours"></option>
</select><br/>
</div>
<div class="form-row inject-time-row hidden" id="inject-time-row-interval-time">
<span data-i18n="inject.every"></span> <select style="width:90px; margin-left:20px;" id="inject-time-interval-time-units" class="inject-time-int-count" value="1">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="10">10</option>
<option value="12">12</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="0">60</option>
</select> <span data-i18n="inject.minutes"></span><br/>
<span data-i18n="inject.between"></span> <select id="inject-time-interval-time-start" class="inject-time-times"></select>
<span data-i18n="inject.and"></span> <select id="inject-time-interval-time-end" class="inject-time-times"></select><br/>
<div id="inject-time-interval-time-days" class="inject-time-days" style="margin-top:5px">
<div style="display:inline-block; vertical-align:top; margin-right:5px;" data-i18n="inject.on">on</div>
<div style="display:inline-block;">
<div>
<label><input type='checkbox' checked value='1'/> <span data-i18n="inject.days.0"></span></label>
<label><input type='checkbox' checked value='2'/> <span data-i18n="inject.days.1"></span></label>
<label><input type='checkbox' checked value='3'/> <span data-i18n="inject.days.2"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='4'/> <span data-i18n="inject.days.3"></span></label>
<label><input type='checkbox' checked value='5'/> <span data-i18n="inject.days.4"></span></label>
<label><input type='checkbox' checked value='6'/> <span data-i18n="inject.days.5"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='0'/> <span data-i18n="inject.days.6"></span></label>
</div>
</div>
</div>
</div>
<div class="form-row inject-time-row hidden" id="inject-time-row-time">
<span data-i18n="inject.at"></span> <input type="text" id="inject-time-time" value="12:00"></input><br/>
<div id="inject-time-time-days" class="inject-time-days">
<div style="display:inline-block; vertical-align:top; margin-right:5px;" data-i18n="inject.on"></div>
<div style="display:inline-block;">
<div>
<label><input type='checkbox' checked value='1'/> <span data-i18n="inject.days.0"></span></label>
<label><input type='checkbox' checked value='2'/> <span data-i18n="inject.days.1"></span></label>
<label><input type='checkbox' checked value='3'/> <span data-i18n="inject.days.2"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='4'/> <span data-i18n="inject.days.3"></span></label>
<label><input type='checkbox' checked value='5'/> <span data-i18n="inject.days.4"></span></label>
<label><input type='checkbox' checked value='6'/> <span data-i18n="inject.days.5"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='0'/> <span data-i18n="inject.days.6"></span></label>
</div>
</div>
</div>
</div>
</script>
<style>
.inject-time-row {
padding-left: 110px;
}
.inject-time-row select {
margin: 3px 0;
}
.inject-time-days label {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: baseline;
width: 100px;
}
.inject-time-days input {
width: auto !important;
vertical-align: baseline !important;
}
.inject-time-times {
width: 90px !important;
}
#inject-time-time {
width: 75px;
margin-left: 8px;
margin-bottom: 8px;
}
.inject-time-count {
padding-left: 3px !important;
width: 80px !important;
}
</style>
<script type="text/javascript">
(function() {
function resizeDialog(size) {
size = size || { height: $(".red-ui-tray-content form").height() }
var rows = $("#dialog-form>div:not(.node-input-property-container-row):visible");
var height = size.height;
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-property-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
height += 16;
$("#node-input-property-container").editableList('height',height);
}
/** Retrieve editableList items (refactored for re-use in the form inject button)*/
function getProps(el, legacy) {
var result = {
props: []
}
el.each(function(i) {
var prop = $(this);
var p = {
p:prop.find(".node-input-prop-property-name").typedInput('value')
};
if (p.p) {
p.v = prop.find(".node-input-prop-property-value").typedInput('value');
p.vt = prop.find(".node-input-prop-property-value").typedInput('type');
if(legacy) {
if (p.p === "payload") { // save payload to old "legacy" property
result.payloadType = p.vt;
result.payload = p.v;
delete p.v;
delete p.vt;
} else if (p.p === "topic" && p.vt === "str") {
result.topic = p.v;
delete p.v;
}
}
result.props.push(p);
}
});
return result;
}
/** Perform inject, optionally sending a custom msg (refactored for re-use in the form inject button)*/
function doInject(node, customMsg) {
var label = node._def.label.call(node,customMsg?customMsg.__user_inject_props__:undefined);
if (label.length > 30) {
label = label.substring(0, 50) + "...";
}
label = label.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify(customMsg||{}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node._("inject.success", { label: label }), { type: "success", id: "inject", timeout: 2000 });
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 404) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.not-deployed") }), "error");
} else if (jqXHR.status == 500) {
RED.notify(node._("common.notification.error", { message: node._("inject.errors.failed") }), "error");
} else if (jqXHR.status == 0) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.no-response") }), "error");
} else {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.unexpected", { status: jqXHR.status, message: textStatus }) }), "error");
}
}
});
}
RED.nodes.registerType('inject',{  
category: 'common',
color:"#a6bbcf",
defaults: {
name: {value:""},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v) {
if (!v || v.length === 0) { return true }
for (var i=0;i<v.length;i++) {
if (/msg|flow|global/.test(v[i].vt)) {
if (!RED.utils.validatePropertyExpression(v[i].v)) {
return false;
}
} else if (v[i].vt === "jsonata") {
try{jsonata(v[i].v);}catch(e){return false;}
} else if (v[i].vt === "json") {
try{JSON.parse(v[i].v);}catch(e){return false;}
}
}
return true;
}
},
repeat: {value:"", validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 2147483))) }},
crontab: {value:""},
once: {value:false},
onceDelay: {value:0.1},
topic: {value:""},
payload: {value:"", validate: RED.validators.typedInput("payloadType")},
payloadType: {value:"date"},
},
icon: "inject.svg",
inputs:0,
outputs:1,
outputLabels: function(index) {
var lab = '';
// if only payload and topic - display payload type
// if only one property - show it's type
// if more than one property (other than payload and topic) - show "x properties" where x is the number of properties.
// this.props will not be an array for legacy inject nodes until they are re-deployed
//
var props = this.props;
if (!Array.isArray(props)) {
props = [
{ p:"payload", v: this.payload, vt: this.payloadType },
{ p:"topic", v: this.topic, vt: "str" }
]
}
if (props) {
for (var i=0,l=props.length; i<l; i++) {
if (i > 0) lab += "\n";
if (i === 5) {
lab += "... +"+(props.length-5);
break;
}
lab += props[i].p+": ";
var propType = props[i].p === "payload"? this.payloadType : props[i].vt;
if (propType === "json") {
try {
var parsedProp = JSON.parse(props[i].p === "payload"? this.payload : props[i].v);
propType = typeof parsedProp;
if (propType === "object" && Array.isArray(parsedProp)) {
propType = "Array";
}
} catch(e) {
propType = "invalid";
}
}
lab += this._("inject.label."+propType);
}
}
return lab;
},
label: function(customProps) {
var suffix = "";
// if fire once then add small indication
if (this.once) {
suffix = " ¹";
}
// but replace with repeat one if set to repeat
if ((this.repeat && this.repeat != 0) || this.crontab) {
suffix = " ↻";
}
if (this.name) {
return this.name+suffix;
}
var payload = "";
var payloadType = "str";
var topic = "";
if (customProps) {
for (var i=0;i<customProps.length;i++) {
if (customProps[i].p === "payload") {
payload = customProps[i].v;
payloadType = customProps[i].vt;
} else if (customProps[i].p === "topic") {
topic = customProps[i].v;
}
}
} else {
payload = this.payload || "";
payloadType = this.payloadType || "str";
topic = this.topic || "";
}
if (payloadType === "string" ||
payloadType === "str" ||
payloadType === "num" ||
payloadType === "bool" ||
payloadType === "json") {
if ((topic !== "") && ((topic.length + payload.length) <= 32)) {
return topic + ":" + payload+suffix;
} else if (payload.length > 0 && payload.length < 24) {
return payload+suffix;
} else {
return this._("inject.inject")+suffix;
}
} else if (payloadType === 'date' || payloadType === 'bin' || payloadType === 'env') {
if ((topic !== "") && (topic.length <= 16)) {
return topic + ":" + this._('inject.label.'+payloadType)+suffix;
} else {
return this._('inject.label.'+payloadType)+suffix;
}
} else if (payloadType === 'flow' || payloadType === 'global') {
var key = RED.utils.parseContextKey(payload);
return payloadType+"."+key.key+suffix;
} else {
return this._("inject.inject")+suffix;
}
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var node = this;
var payloadType = node.payloadType;
if (node.payloadType == null) {
if (node.payload == "") {
payloadType = "date";
} else {
payloadType = "str";
}
} else if (node.payloadType === 'string' || node.payloadType === 'none') {
payloadType = "str";
}
$("#inject-time-type-select").on("change", function() {
$("#node-input-crontab").val('');
var id = $("#inject-time-type-select").val();
$(".inject-time-row").hide();
$("#inject-time-row-"+id).show();
if ((id == "none") || (id == "interval") || (id == "interval-time")) {
$("#node-once").show();
}
else {
$("#node-once").hide();
$("#node-input-once").prop('checked', false);
}
// Scroll down
var scrollDiv = $("#dialog-form").parent();
scrollDiv.scrollTop(scrollDiv.prop('scrollHeight'));
resizeDialog();
});
$("#node-input-once").on("change", function() {
$("#node-input-onceDelay").attr('disabled', !$("#node-input-once").prop('checked'));
})
$(".inject-time-times").each(function() {
for (var i=0; i<24; i++) {
var l = (i<10?"0":"")+i+":00";
$(this).append($("<option></option>").val(i).text(l));
}
});
$("<option></option>").val(24).text("00:00").appendTo("#inject-time-interval-time-end");
$("#inject-time-interval-time-start").on("change", function() {
var start = Number($("#inject-time-interval-time-start").val());
var end = Number($("#inject-time-interval-time-end").val());
$("#inject-time-interval-time-end option").remove();
for (var i=start+1; i<25; i++) {
var l = (i<10?"0":"")+i+":00";
if (i==24) {
l = "00:00";
}
var opt = $("<option></option>").val(i).text(l).appendTo("#inject-time-interval-time-end");
if (i === end) {
opt.attr("selected","selected");
}
}
});
$(".inject-time-count").spinner({
//max:60,
min:1
});
var repeattype = "none";
if (node.repeat != "" && node.repeat != 0) {
repeattype = "interval";
var r = "s";
var c = node.repeat;
if (node.repeat % 60 === 0) { r = "m"; c = c/60; }
if (node.repeat % 1440 === 0) { r = "h"; c = c/60; }
$("#inject-time-interval-count").val(c);
$("#inject-time-interval-units").val(r);
$("#inject-time-interval-days").prop("disabled","disabled");
} else if (node.crontab) {
var cronparts = node.crontab.split(" ");
var days = cronparts[4];
if (!isNaN(cronparts[0]) && !isNaN(cronparts[1])) {
repeattype = "time";
// Fixed time
var time = cronparts[1]+":"+cronparts[0];
$("#inject-time-time").val(time);
$("#inject-time-type-select").val("s");
if (days == "*") {
$("#inject-time-time-days input[type=checkbox]").prop("checked",true);
} else {
$("#inject-time-time-days input[type=checkbox]").removeAttr("checked");
days.split(",").forEach(function(v) {
$("#inject-time-time-days [value=" + v + "]").prop("checked", true);
});
}
} else {
repeattype = "interval-time";
// interval - time period
var minutes = cronparts[0].slice(2);
if (minutes === "") { minutes = "0"; }
$("#inject-time-interval-time-units").val(minutes);
if (days == "*") {
$("#inject-time-interval-time-days input[type=checkbox]").prop("checked",true);
} else {
$("#inject-time-interval-time-days input[type=checkbox]").removeAttr("checked");
days.split(",").forEach(function(v) {
$("#inject-time-interval-time-days [value=" + v + "]").prop("checked", true);
});
}
var time = cronparts[1];
var timeparts = time.split(",");
var start;
var end;
if (timeparts.length == 1) {
// 0 or 0-10
var hours = timeparts[0].split("-");
if (hours.length == 1) {
if (hours[0] === "") {
start = "0";
end = "0";
}
else {
start = hours[0];
end = Number(hours[0])+1;
}
} else {
start = hours[0];
end = Number(hours[1])+1;
}
} else {
// 23,0 or 17-23,0-10 or 23,0-2 or 17-23,0
var startparts = timeparts[0].split("-");
start = startparts[0];
var endparts = timeparts[1].split("-");
if (endparts.length == 1) {
end = Number(endparts[0])+1;
} else {
end = Number(endparts[1])+1;
}
}
$("#inject-time-interval-time-end").val(end);
$("#inject-time-interval-time-start").val(start);
}
} else {
$("#inject-time-type-select").val("none");
}
$(".inject-time-row").hide();
$("#inject-time-type-select").val(repeattype);
$("#inject-time-row-"+repeattype).show();
/* */
var eList = $('#node-input-property-container').css('min-height','120px').css('min-width','450px');
eList.editableList({
buttons: [
{
id: "node-inject-test-inject-button",
label: node._("inject.injectNow"),
click: function(e) {
var items = eList.editableList('items');
var props = getProps(items);
var m = {__user_inject_props__: props.props};
doInject(node, m);
}
}
],
addItem: function(container,i,opt) {
var prop = opt;
if (!prop.hasOwnProperty('p')) {
prop = {p:"",v:"",vt:"str"};
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
var row = $('<div/>').appendTo(container);
var propertyName = $('<input/>',{class:"node-input-prop-property-name",type:"text"})
.css("width","30%")
.appendTo(row)
.typedInput({types:['msg']});
$('<div/>',{style: 'display:inline-block; padding:0px 6px;'})
.text('=')
.appendTo(row);
var propertyValue = $('<input/>',{class:"node-input-prop-property-value",type:"text"})
.css("width","calc(70% - 30px)")
.appendTo(row)
.typedInput({default:prop.vt || 'str',types:['flow','global','str','num','bool','json','bin','date','jsonata','env','msg']});
propertyName.typedInput('value',prop.p);
propertyValue.typedInput('value',prop.v);
},
removable: true,
sortable: true
});
$('#node-inject-test-inject-button').css("float", "right").css("margin-right", "unset");
if (RED.nodes.subflow(node.z)) {
$('#node-inject-test-inject-button').attr("disabled",true);
}
if (!node.props) {
var payload = {
p:'payload',
v: node.payload ? node.payload : '',
vt:payloadType ? payloadType : 'date'
};
var topic = {
p:'topic',
v: node.topic ? node.topic : '',
vt:'str'
}
node.props = [payload,topic];
}
for (var i=0; i<node.props.length; i++) {
var prop = node.props[i];
var newProp = { p: prop.p, v: prop.v, vt: prop.vt };
if (newProp.v === undefined) {
if (prop.p === 'payload') {
newProp.v = node.payload ? node.payload : '';
newProp.vt = payloadType ? payloadType : 'date';
} else if (prop.p === 'topic' && prop.vt === "str") {
newProp.v = node.topic ? node.topic : '';
}
}
if (newProp.vt === "string") {
// Fix bug in pre 2.1 where an old Inject node might have
// a migrated rule with type 'string' not 'str'
newProp.vt = "str";
}
eList.editableList('addItem',newProp);
}
$("#inject-time-type-select").trigger("change");
$("#inject-time-interval-time-start").trigger("change");
},
oneditsave: function() {
var repeat = "";
var crontab = "";
var type = $("#inject-time-type-select").val();
if (type == "none") {
// nothing
} else if (type == "interval") {
var count = $("#inject-time-interval-count").val();
var units = $("#inject-time-interval-units").val();
if (units == "s") {
repeat = count;
} else {
if (units == "m") {
//crontab = "*/"+count+" * * * "+days;
repeat = count * 60;
} else if (units == "h") {
//crontab = "0 */"+count+" * * "+days;
repeat = count * 60 * 60;
}
}
} else if (type == "interval-time") {
repeat = "";
var count = $("#inject-time-interval-time-units").val();
var startTime = Number($("#inject-time-interval-time-start").val());
var endTime = Number($("#inject-time-interval-time-end").val());
var days = $('#inject-time-interval-time-days input[type=checkbox]:checked').map(function(_, el) {
return $(el).val()
}).get();
if (days.length == 0) {
crontab = "";
} else {
if (days.length == 7) {
days="*";
} else {
days = days.join(",");
}
var timerange = "";
if (endTime == 0) {
timerange = startTime+"-23";
} else if (startTime+1 < endTime) {
timerange = startTime+"-"+(endTime-1);
} else if (startTime+1 == endTime) {
timerange = startTime;
} else {
var startpart = "";
var endpart = "";
if (startTime == 23) {
startpart = "23";
} else {
startpart = startTime+"-23";
}
if (endTime == 1) {
endpart = "0";
} else {
endpart = "0-"+(endTime-1);
}
timerange = startpart+","+endpart;
}
if (count === "0") {
crontab = count+" "+timerange+" * * "+days;
} else {
crontab = "*/"+count+" "+timerange+" * * "+days;
}
}
} else if (type == "time") {
var time = $("#inject-time-time").val();
var days = $('#inject-time-time-days input[type=checkbox]:checked').map(function(_, el) {
return $(el).val()
}).get();
if (days.length == 0) {
crontab = "";
} else {
if (days.length == 7) {
days="*";
} else {
days = days.join(",");
}
var parts = time.split(":");
if (parts.length === 2) {
repeat = "";
parts[1] = ("00" + (parseInt(parts[1]) % 60)).substr(-2);
parts[0] = ("00" + (parseInt(parts[0]) % 24)).substr(-2);
crontab = parts[1]+" "+parts[0]+" * * "+days;
}
else { crontab = ""; }
}
}
$("#node-input-repeat").val(repeat);
$("#node-input-crontab").val(crontab);
/* Gather the properties */
var items = $("#node-input-property-container").editableList('items');
delete this.payloadType;
delete this.payload;
this.topic = "";
var result = getProps(items, true);
this.props = result.props;
if(result.hasOwnProperty('payloadType')) { this.payloadType = result.payloadType; };
if(result.hasOwnProperty('payload')) { this.payload = result.payload; };
if(result.hasOwnProperty('topic')) { this.topic = result.topic; };
},
button: {
enabled: function() {
return !this.changed
},
onclick: function () {
if (this.changed) {
return RED.notify(RED._("notification.warning", { message: RED._("notification.warnings.undeployedChanges") }), "warning");
}
doInject(this);
}
},
oneditresize: resizeDialog
});
})();
</script>

View File

@ -0,0 +1,67 @@
<!-- 2022 Modification Copyright - Defense Unicorns -->
<script type="text/html" data-template-name="guide">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row node-text-editor-row">
<input type="hidden" id="node-input-info" autofocus="autofocus">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-info-editor"></div>
</div>
</script>
<script type="text/x-red" data-help-name="guide">
<p>A node that contains a step by step guide on how to do something.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('guide',{
category: 'common',
color:"#F19941",
defaults: {
name: {value:""},
info: {value:"# Description\n\n# Resources\n\n# Unicorn SME(s)\n"}
},
inputs:1,
outputs:1,
icon: "guide.svg",
label: function() {
return this.name||this._("guide");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
info: function() {
return this.name?"# "+this.name+"\n\n---\n\n":"";
},
oneditprepare: function() {
var that = this;
this.editor = RED.editor.createEditor({
id: 'node-input-info-editor',
mode: 'ace/mode/markdown',
value: $("#node-input-info").val()
});
this.editor.focus();
},
oneditsave: function() {
$("#node-input-info").val(this.editor.getValue());
this.editor.destroy();
delete this.editor;
},
oneditcancel: function() {
this.editor.destroy();
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>

View File

@ -0,0 +1,25 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
// 2022 Modification Copyright - Defense Unicorns
module.exports = function(RED) {
"use strict";
function GuideNode(n) {
RED.nodes.createNode(this,n);
}
RED.nodes.registerType("guide",GuideNode);
}

View File

@ -0,0 +1,67 @@
<!-- 2022 Modification Copyright - Defense Unicorns -->
<script type="text/html" data-template-name="resource">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row node-text-editor-row">
<input type="hidden" id="node-input-info" autofocus="autofocus">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-info-editor"></div>
</div>
</script>
<script type="text/x-red" data-help-name="resource">
<p>A node that contains an external resource or explains something.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('resource',{
category: 'common',
color:"#7FFFD4",
defaults: {
name: {value:""},
info: {value:"# Description\n\n# Resources\n\n# Unicorn SME(s)\n"}
},
inputs:1,
outputs:1,
icon: "db.svg",
label: function() {
return this.name||this._("resource");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
info: function() {
return this.name?"# "+this.name+"\n\n---\n\n":"";
},
oneditprepare: function() {
var that = this;
this.editor = RED.editor.createEditor({
id: 'node-input-info-editor',
mode: 'ace/mode/markdown',
value: $("#node-input-info").val()
});
this.editor.focus();
},
oneditsave: function() {
$("#node-input-info").val(this.editor.getValue());
this.editor.destroy();
delete this.editor;
},
oneditcancel: function() {
this.editor.destroy();
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>

View File

@ -0,0 +1,25 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
// 2022 Modification Copyright - Defense Unicorns
module.exports = function(RED) {
"use strict";
function ResourceNode(n) {
RED.nodes.createNode(this,n);
}
RED.nodes.registerType("resource",ResourceNode);
}

View File

@ -0,0 +1,67 @@
<!-- 2022 Modification Copyright - Defense Unicorns -->
<script type="text/html" data-template-name="task">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row node-text-editor-row">
<input type="hidden" id="node-input-info" autofocus="autofocus">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-info-editor"></div>
</div>
</script>
<script type="text/x-red" data-help-name="task">
<p>A node that is used when assigning a non-guided task.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('task',{
category: 'common',
color:"#cc3333",
defaults: {
name: {value:""},
info: {value:"# Description\n\n# Resources\n\n# Unicorn SME(s)\n"}
},
inputs:1,
outputs:1,
icon: "cog.svg",
label: function() {
return this.name||this._("task");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
info: function() {
return this.name?"# "+this.name+"\n\n---\n\n":"";
},
oneditprepare: function() {
var that = this;
this.editor = RED.editor.createEditor({
id: 'node-input-info-editor',
mode: 'ace/mode/markdown',
value: $("#node-input-info").val()
});
this.editor.focus();
},
oneditsave: function() {
$("#node-input-info").val(this.editor.getValue());
this.editor.destroy();
delete this.editor;
},
oneditcancel: function() {
this.editor.destroy();
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>

View File

@ -0,0 +1,25 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
// 2022 Modification Copyright - Defense Unicorns
module.exports = function(RED) {
"use strict";
function TaskNode(n) {
RED.nodes.createNode(this,n);
}
RED.nodes.registerType("task",TaskNode);
}

View File

@ -1,611 +0,0 @@
<script type="text/html" data-template-name="function">
<style>
.func-tabs-row {
margin-bottom: 0;
}
#node-input-libs-container-row .red-ui-editableList-container {
padding: 0px;
}
#node-input-libs-container-row .red-ui-editableList-container li {
padding:0px;
}
#node-input-libs-container-row .red-ui-editableList-item-remove {
right: 5px;
}
#node-input-libs-container-row .red-ui-editableList-header {
display: flex;
background: var(--red-ui-tertiary-background);
padding-right: 75px;
}
#node-input-libs-container-row .red-ui-editableList-header > div {
flex-grow: 1;
}
.node-libs-entry {
display: flex;
}
.node-libs-entry .red-ui-typedInput-container {
border-radius: 0;
border: none;
}
.node-libs-entry .red-ui-typedInput-type-select {
border-radius: 0 !important;
height: 34px;
}
.node-libs-entry > span > input[type=text] {
border-radius: 0;
border-top-color: var(--red-ui-form-background);
border-bottom-color: var(--red-ui-form-background);
border-right-color: var(--red-ui-form-background);
}
.node-libs-entry > span > input[type=text].input-error {
}
.node-libs-entry > span {
flex-grow: 1;
width: 50%;
position: relative;
}
.node-libs-entry span .node-input-libs-var, .node-libs-entry span .red-ui-typedInput-container {
width: 100%;
}
.node-libs-entry > span > span > i {
display: none;
}
.node-libs-entry > span > span.input-error > i {
display: inline;
}
</style>
<input type="hidden" id="node-input-func">
<input type="hidden" id="node-input-noerr">
<input type="hidden" id="node-input-finalize">
<input type="hidden" id="node-input-initialize">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
</div>
<div class="form-row func-tabs-row">
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
</div>
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
<div id="func-tab-config" style="display:none">
<div class="form-row">
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
<input id="node-input-outputs" style="width: 60px;" value="1">
</div>
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
<label><i class="fa fa-cubes"></i> <span data-i18n="function.label.modules"></span></label>
</div>
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
<ol id="node-input-libs-container"></ol>
</div>
</div>
<div id="func-tab-init" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-finalize" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
</div>
</script>
<script type="text/javascript">
(function() {
var invalidModuleVNames = [
'console',
'util',
'Buffer',
'Date',
'RED',
'node',
'__node__',
'context',
'flow',
'global',
'env',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'promisify'
]
var knownFunctionNodes = {};
RED.events.on("nodes:add", function(n) {
if (n.type === "function") {
knownFunctionNodes[n.id] = n;
}
})
RED.events.on("nodes:remove", function(n) {
if (n.type === "function") {
delete knownFunctionNodes[n.id];
}
})
var missingModules = [];
var missingModuleReasons = {};
RED.events.on("runtime-state", function(event) {
if (event.error === "missing-modules") {
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
} else if (!event.text) {
missingModuleReasons = {};
missingModules = [];
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
}
RED.view.redraw();
});
var installAllowList = ['*'];
var installDenyList = [];
var modulesEnabled = true;
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
modulesEnabled = false;
}
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
if (settingsAllowList || settingsDenyList) {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
// object that maps from library name to its descriptor
var allLibs = [];
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
function getAllUsedModules() {
var moduleSet = new Set();
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id)) {
if (knownFunctionNodes[id].libs) {
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
moduleSet.add(knownFunctionNodes[id].libs[i].module);
}
}
}
}
}
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$(".node-input-libs-row").show();
var usedModules = getAllUsedModules();
var typedModules = usedModules.map(function(l) {
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
})
typedModules.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
header: $('<div><div data-i18n="node-red:function.require.moduleName"></div><div data-i18n="node-red:function.require.importAs"></div></div>'),
addItem: function(container,i,opt) {
var parent = container.parent();
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
var fmoduleSpan = $("<span>").appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text"
}).css({
}).appendTo(fmoduleSpan).typedInput({
types: typedModules,
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
});
if (usedModules.indexOf(opt.module) === -1) {
fmodule.typedInput('value', opt.module);
}
var moduleWarning = $('<span style="position: absolute;right:2px;top:7px; display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fmoduleSpan);
RED.popover.tooltip(moduleWarning.find("i"),function() {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
var errors = [];
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
return RED._("node-red:function.error.moduleNotAllowed",{module:val});
} else {
return RED._("node-red:function.error.moduleLoadError",{module:val,error:missingModuleReasons[val]});
}
})
var fvarSpan = $("<span>").appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
}).appendTo(fvarSpan).val(opt.var);
var vnameWarning = $('<span style="position: absolute; right:2px;top:7px;display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fvarSpan);
RED.popover.tooltip(vnameWarning.find("i"),function() {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved",{name:val})
} else {
return RED._("node-red:function.error.moduleNameError",{name:val})
}
})
fvar.on("change keyup paste", function (e) {
var v = $(this).val().trim();
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
fvar.addClass("input-error");
vnameWarning.addClass("input-error");
} else {
fvar.removeClass("input-error");
vnameWarning.removeClass("input-error");
}
});
fmodule.on("change keyup paste", function (e) {
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/].?/g, function(v) { return v[1]?v[1].toUpperCase():"" });
fvar.val(varName);
fvar.trigger("change");
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true
});
var libs = node.libs || [];
for (var i=0,l=libs.length;i<l; i++) {
libList.editableList('addItem',libs[i])
}
}
function getLibsList() {
var _libs = [];
if (RED.settings.functionExternalModules !== false) {
var libs = $("#node-input-libs-container").editableList("items");
libs.each(function(i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").typedInput("type");
if (n === "_custom_") {
n = item.find(".node-input-libs-val").val();
}
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
_libs.push({
var: v,
module: n
});
});
}
return _libs;
}
RED.nodes.registerType('function',{
color:"#fdd0a2",
category: 'function',
defaults: {
name: {value:""},
func: {value:"\nreturn msg;"},
outputs: {value:1},
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""},
libs: {value: [], validate: function(v) {
if (!v) { return true; }
for (var i=0,l=v.length;i<l;i++) {
var m = v[i];
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
return false
}
if (m.var === "" || / /.test(m.var)) {
return false;
}
if (missingModules.indexOf(m.module) > -1) {
return false;
}
if (invalidModuleVNames.indexOf(m.var) !== -1){
return false;
}
}
return true;
}}
},
inputs:1,
outputs:1,
icon: "function.svg",
label: function() {
return this.name||this._("function.function");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var that = this;
var tabs = RED.tabs.create({
id: "func-tabs",
onchange: function(tab) {
$("#func-tabs-content").children().hide();
$("#" + tab.id).show();
let editor = $("#" + tab.id).find('.monaco-editor').first();
if(editor.length) {
if(that.editor.nodered && that.editor.type == "monaco") {
that.editor.nodered.refreshModuleLibs(getLibsList());
}
RED.tray.resize();
}
}
});
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
label: that._("function.label.setup")
});
tabs.addTab({
id: "func-tab-init",
label: that._("function.label.initialize")
});
tabs.addTab({
id: "func-tab-body",
label: that._("function.label.function")
});
tabs.addTab({
id: "func-tab-finalize",
label: that._("function.label.finalize")
});
tabs.activateTab("func-tab-body");
$( "#node-input-outputs" ).spinner({
min:0,
change: function(event, ui) {
var value = this.value;
if (!value.match(/^\d+$/)) { value = 1; }
else if (value < this.min) { value = this.min; }
if (value !== this.value) { $(this).spinner("value", value); }
}
});
var buildEditor = function(id, value, defaultValue, extraLibs) {
var editor = RED.editor.createEditor({
id: id,
mode: 'ace/mode/nrjavascript',
value: value || defaultValue || "",
globals: {
msg:true,
context:true,
RED: true,
util: true,
flow: true,
global: true,
console: true,
Buffer: true,
setTimeout: true,
clearTimeout: true,
setInterval: true,
clearInterval: true
},
extraLibs: extraLibs
});
if (defaultValue && value === "") {
editor.moveCursorTo(defaultValue.split("\n").length - 1, 0);
}
return editor;
}
this.initEditor = buildEditor('node-input-init-editor',$("#node-input-initialize").val(),RED._("node-red:function.text.initialize"))
this.editor = buildEditor('node-input-func-editor',$("#node-input-func").val(), undefined, that.libs || [])
this.finalizeEditor = buildEditor('node-input-finalize-editor',$("#node-input-finalize").val(),RED._("node-red:function.text.finalize"))
RED.library.create({
url:"functions", // where to get the data from
type:"function", // the type of object the library is for
editor:this.editor, // the field name the main text body goes to
mode:"ace/mode/nrjavascript",
fields:[
'name', 'outputs',
{
name: 'initialize',
get: function() {
return that.initEditor.getValue();
},
set: function(v) {
that.initEditor.setValue(v||RED._("node-red:function.text.initialize"), -1);
}
},
{
name: 'finalize',
get: function() {
return that.finalizeEditor.getValue();
},
set: function(v) {
that.finalizeEditor.setValue(v||RED._("node-red:function.text.finalize"), -1);
}
},
{
name: 'info',
get: function() {
return that.infoEditor.getValue();
},
set: function(v) {
that.infoEditor.setValue(v||"", -1);
}
}
],
ext:"js"
});
this.editor.focus();
var expandButtonClickHandler = function(editor) {
return function(e) {
e.preventDefault();
var value = editor.getValue();
var extraLibs = that.libs || [];
RED.editor.editJavaScript({
value: value,
width: "Infinity",
cursor: editor.getCursorPosition(),
mode: "ace/mode/nrjavascript",
complete: function(v,cursor) {
editor.setValue(v, -1);
editor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() {
editor.focus();
},300);
},
extraLibs: extraLibs
})
}
}
$("#node-init-expand-js").on("click", expandButtonClickHandler(this.initEditor));
$("#node-function-expand-js").on("click", expandButtonClickHandler(this.editor));
$("#node-finalize-expand-js").on("click", expandButtonClickHandler(this.finalizeEditor));
RED.popover.tooltip($("#node-init-expand-js"), RED._("node-red:common.label.expand"));
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
},
oneditsave: function() {
var node = this;
var noerr = 0;
$("#node-input-noerr").val(0);
var disposeEditor = function(editorName,targetName,defaultValue) {
var editor = node[editorName];
var annot = editor.getSession().getAnnotations();
for (var k=0; k < annot.length; k++) {
if (annot[k].type === "error") {
noerr += annot.length;
break;
}
}
var val = editor.getValue();
if (defaultValue) {
if (val.trim() == defaultValue.trim()) {
val = "";
}
}
editor.destroy();
delete node[editorName];
$("#"+targetName).val(val);
}
disposeEditor("editor","node-input-func");
disposeEditor("initEditor","node-input-initialize", RED._("node-red:function.text.initialize"));
disposeEditor("finalizeEditor","node-input-finalize", RED._("node-red:function.text.finalize"));
$("#node-input-noerr").val(noerr);
this.noerr = noerr;
node.libs = getLibsList();
},
oneditcancel: function() {
var node = this;
node.editor.destroy();
delete node.editor;
node.initEditor.destroy();
delete node.initEditor;
node.finalizeEditor.destroy();
delete node.finalizeEditor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#dialog-form .node-text-editor").css("height",height+"px");
var height = size.height;
$("#node-input-init-editor").css("height", (height - 83)+"px");
$("#node-input-func-editor").css("height", (height - 83)+"px");
$("#node-input-finalize-editor").css("height", (height - 83)+"px");
this.initEditor.resize();
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height - 192)+"px");
}
});
})();
</script>

View File

@ -1,510 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
var util = require("util");
var vm = require("vm");
var acorn = require("acorn");
var acornWalk = require("acorn-walk");
function sendResults(node,send,_msgid,msgs,cloneFirstMessage) {
if (msgs == null) {
return;
} else if (!util.isArray(msgs)) {
msgs = [msgs];
}
var msgCount = 0;
for (var m=0; m<msgs.length; m++) {
if (msgs[m]) {
if (!util.isArray(msgs[m])) {
msgs[m] = [msgs[m]];
}
for (var n=0; n < msgs[m].length; n++) {
var msg = msgs[m][n];
if (msg !== null && msg !== undefined) {
if (typeof msg === 'object' && !Buffer.isBuffer(msg) && !util.isArray(msg)) {
if (msgCount === 0 && cloneFirstMessage !== false) {
msgs[m][n] = RED.util.cloneMessage(msgs[m][n]);
msg = msgs[m][n];
}
msg._msgid = _msgid;
msgCount++;
} else {
var type = typeof msg;
if (type === 'object') {
type = Buffer.isBuffer(msg)?'Buffer':(util.isArray(msg)?'Array':'Date');
}
node.error(RED._("function.error.non-message-returned",{ type: type }));
}
}
}
}
}
if (msgCount>0) {
send(msgs);
}
}
function createVMOpt(node, kind) {
var opt = {
filename: 'Function node'+kind+':'+node.id+(node.name?' ['+node.name+']':''), // filename for stack traces
displayErrors: true
// Using the following options causes node 4/6 to not include the line number
// in the stack output. So don't use them.
// lineOffset: -11, // line number offset to be used for stack traces
// columnOffset: 0, // column number offset to be used for stack traces
};
return opt;
}
function updateErrorInfo(err) {
if (err.stack) {
var stack = err.stack.toString();
var m = /^([^:]+):([^:]+):(\d+).*/.exec(stack);
if (m) {
var line = parseInt(m[3]) -1;
var kind = "body:";
if (/setup/.exec(m[1])) {
kind = "setup:";
}
if (/cleanup/.exec(m[1])) {
kind = "cleanup:";
}
err.message += " ("+kind+"line "+line+")";
}
}
}
function FunctionNode(n) {
RED.nodes.createNode(this,n);
var node = this;
node.name = n.name;
node.func = n.func;
node.outputs = n.outputs;
node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = n.libs || [];
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
throw new Error(RED._("function.error.externalModuleNotAllowed"));
}
var functionText = "var results = null;"+
"results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+
"var node = {"+
"id:__node__.id,"+
"name:__node__.name,"+
"path:__node__.path,"+
"outputCount:__node__.outputCount,"+
"log:__node__.log,"+
"error:__node__.error,"+
"warn:__node__.warn,"+
"debug:__node__.debug,"+
"trace:__node__.trace,"+
"on:__node__.on,"+
"status:__node__.status,"+
"send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+
"done:__done__"+
"};\n"+
node.func+"\n"+
"})(msg,__send__,__done__);";
var handleNodeDoneCall = true;
// Check to see if the Function appears to call `node.done()`. If so,
// we will assume it is well written and does actually call node.done().
// Otherwise, we will call node.done() after the function returns regardless.
if (/node\.done\s*\(\s*\)/.test(functionText)) {
// We have spotted the code contains `node.done`. It could be in a comment
// so need to do the extra work to parse the AST and examine it properly.
acornWalk.simple(acorn.parse(functionText,{ecmaVersion: "latest"} ), {
CallExpression(astNode) {
if (astNode.callee && astNode.callee.object) {
if (astNode.callee.object.name === "node" && astNode.callee.property.name === "done") {
handleNodeDoneCall = false;
}
}
}
})
}
var finScript = null;
var finOpt = null;
node.topic = n.topic;
node.outstandingTimers = [];
node.outstandingIntervals = [];
node.clearStatus = false;
var sandbox = {
console:console,
util:util,
Buffer:Buffer,
Date: Date,
RED: {
util: RED.util
},
__node__: {
id: node.id,
name: node.name,
path: node._path,
outputCount: node.outputs,
log: function() {
node.log.apply(node, arguments);
},
error: function() {
node.error.apply(node, arguments);
},
warn: function() {
node.warn.apply(node, arguments);
},
debug: function() {
node.debug.apply(node, arguments);
},
trace: function() {
node.trace.apply(node, arguments);
},
send: function(send, id, msgs, cloneMsg) {
sendResults(node, send, id, msgs, cloneMsg);
},
on: function() {
if (arguments[0] === "input") {
throw new Error(RED._("function.error.inputListener"));
}
node.on.apply(node, arguments);
},
status: function() {
node.clearStatus = true;
node.status.apply(node, arguments);
}
},
context: {
set: function() {
node.context().set.apply(node,arguments);
},
get: function() {
return node.context().get.apply(node,arguments);
},
keys: function() {
return node.context().keys.apply(node,arguments);
},
get global() {
return node.context().global;
},
get flow() {
return node.context().flow;
}
},
flow: {
set: function() {
node.context().flow.set.apply(node,arguments);
},
get: function() {
return node.context().flow.get.apply(node,arguments);
},
keys: function() {
return node.context().flow.keys.apply(node,arguments);
}
},
global: {
set: function() {
node.context().global.set.apply(node,arguments);
},
get: function() {
return node.context().global.get.apply(node,arguments);
},
keys: function() {
return node.context().global.keys.apply(node,arguments);
}
},
env: {
get: function(envVar) {
return RED.util.getSetting(node, envVar);
}
},
setTimeout: function () {
var func = arguments[0];
var timerId;
arguments[0] = function() {
sandbox.clearTimeout(timerId);
try {
func.apply(node,arguments);
} catch(err) {
node.error(err,{});
}
};
timerId = setTimeout.apply(node,arguments);
node.outstandingTimers.push(timerId);
return timerId;
},
clearTimeout: function(id) {
clearTimeout(id);
var index = node.outstandingTimers.indexOf(id);
if (index > -1) {
node.outstandingTimers.splice(index,1);
}
},
setInterval: function() {
var func = arguments[0];
var timerId;
arguments[0] = function() {
try {
func.apply(node,arguments);
} catch(err) {
node.error(err,{});
}
};
timerId = setInterval.apply(node,arguments);
node.outstandingIntervals.push(timerId);
return timerId;
},
clearInterval: function(id) {
clearInterval(id);
var index = node.outstandingIntervals.indexOf(id);
if (index > -1) {
node.outstandingIntervals.splice(index,1);
}
}
};
if (util.hasOwnProperty('promisify')) {
sandbox.setTimeout[util.promisify.custom] = function(after, value) {
return new Promise(function(resolve, reject) {
sandbox.setTimeout(function(){ resolve(value); }, after);
});
};
sandbox.promisify = util.promisify;
}
const moduleLoadPromises = [];
if (node.hasOwnProperty("libs")) {
let moduleErrors = false;
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("var") ? module.var : null;
if (vname && (vname !== "")) {
if (sandbox.hasOwnProperty(vname) || vname === 'node') {
node.error(RED._("function.error.moduleNameError",{name:vname}))
moduleErrors = true;
return;
}
sandbox[vname] = null;
var spec = module.module;
if (spec && (spec !== "")) {
moduleLoadPromises.push(RED.import(module.module).then(lib => {
sandbox[vname] = lib.default;
}).catch(err => {
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()}))
throw err;
}));
}
}
});
if (moduleErrors) {
throw new Error(RED._("function.error.externalModuleLoadError"));
}
}
const RESOLVING = 0;
const RESOLVED = 1;
const ERROR = 2;
var state = RESOLVING;
var messages = [];
var processMessage = (() => {});
node.on("input", function(msg,send,done) {
if(state === RESOLVING) {
messages.push({msg:msg, send:send, done:done});
}
else if(state === RESOLVED) {
processMessage(msg, send, done);
}
});
Promise.all(moduleLoadPromises).then(() => {
var context = vm.createContext(sandbox);
try {
var iniScript = null;
var iniOpt = null;
if (node.ini && (node.ini !== "")) {
var iniText = `
(async function(__send__) {
var node = {
id:__node__.id,
name:__node__.name,
path:__node__.path,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
}
};
`+ node.ini +`
})(__initSend__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = `(function () {
var node = {
id:__node__.id,
name:__node__.name,
path:__node__.path,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.error("Cannot send from close function");
}
};
`+node.fin +`
})();`;
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
}
var promise = Promise.resolve();
if (iniScript) {
context.__initSend__ = function(msgs) { node.send(msgs); };
promise = iniScript.runInContext(context, iniOpt);
}
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
done();
}
var duration = process.hrtime(start);
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
node.metric("duration", msg, converted);
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({fill:"yellow",shape:"dot",text:""+converted});
}
}).catch(err => {
if ((typeof err === "object") && err.hasOwnProperty("stack")) {
//remove unwanted part
var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/);
err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n');
var stack = err.stack.split(/\r?\n/);
//store the error in msg to be used in flows
msg.error = err;
var line = 0;
var errorMessage;
if (stack.length > 0) {
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
}
if (line < stack.length) {
errorMessage = stack[line];
var m = /:(\d+):(\d+)$/.exec(stack[line+1]);
if (m) {
var lineno = Number(m[1])-1;
var cha = m[2];
errorMessage += " (line "+lineno+", col "+cha+")";
}
}
}
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);
}
else {
done(JSON.stringify(err));
}
});
}
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
}
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
promise.then(function (v) {
var msgs = messages;
messages = [];
while (msgs.length > 0) {
msgs.forEach(function (s) {
processMessage(s.msg, s.send, s.done);
});
msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
}
catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
updateErrorInfo(err);
node.error(err);
}
}).catch(err => {
node.error(RED._("function.error.externalModuleLoadError"));
});
}
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "libs",
settings: {
functionExternalModules: { value: true, exportable: true }
}
});
RED.library.register("functions");
};

View File

@ -1,284 +0,0 @@
<!--
Copyright JS Foundation and other contributors, http://js.foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/html" data-template-name="delay">
<div class="form-row">
<label for="node-input-delay-action"><i class="fa fa-tasks"></i> <span data-i18n="delay.action"></span></label>
<select id="node-input-delay-action" style="width:270px !important">
<option value="delay" data-i18n="delay.delaymsg"></option>
<option value="rate" data-i18n="delay.limitrate"></option>
</select>
</div>
<div id="delay-details">
<div class="form-row">
<label></label>
<select id="node-input-delay-type" style="width:270px !important">
<option value="delay" data-i18n="delay.delayfixed"></option>
<option value="random" data-i18n="delay.randomdelay"></option>
<option value="delayv" data-i18n="delay.delayvarmsg"></option>
</select>
</div>
<div class="form-row" id="delay-details-for">
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="delay.for"></span></label>
<input type="text" id="node-input-timeout" style="text-align:end; width:50px !important">
<select id="node-input-timeoutUnits" style="width:200px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
<option value="minutes" data-i18n="delay.mins"></option>
<option value="hours" data-i18n="delay.hours"></option>
<option value="days" data-i18n="delay.days"></option>
</select>
</div>
<div id="random-details" class="form-row">
<label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label>
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:50px !important">
&nbsp;<span data-i18n="delay.and"></span>&nbsp;
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:50px !important">
<select id="node-input-randomUnits" style="width:140px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
<option value="minutes" data-i18n="delay.mins"></option>
<option value="hours" data-i18n="delay.hours"></option>
<option value="days" data-i18n="delay.days"></option>
</select>
</div>
</div>
<div id="rate-details">
<div class="form-row">
<label></label>
<select id="node-input-rate-type" style="width:270px !important">
<option value="all" data-i18n="delay.limitall"></option>
<option value="topic" data-i18n="delay.limittopic"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-rate"><i class="fa fa-clock-o"></i> <span data-i18n="delay.rate"></span></label>
<input type="text" id="node-input-rate" placeholder="1" style="text-align:end; width:40px !important">
<label for="node-input-rateUnits"><span data-i18n="delay.msgper"></span></label>
<input type="text" id="node-input-nbRateUnits" placeholder="1" style="text-align:end; width:40px !important">
<select id="node-input-rateUnits" style="width:90px !important">
<option value="second" data-i18n="delay.label.units.second.singular"></option>
<option value="minute" data-i18n="delay.label.units.minute.singular"></option>
<option value="hour" data-i18n="delay.label.units.hour.singular"></option>
<option value="day" data-i18n="delay.label.units.day.singular"></option>
</select>
</div>
<div class="form-row" id="rate-override" style="display: flex; align-items: center">
<label></label><input style="width:30px; margin:0" type="checkbox" id="node-input-allowrate"><label style="margin:0;width: auto;" for="node-input-allowrate" data-i18n="delay.allowrate"></label>
</div>
<div class="form-row" id="rate-details-drop">
<input type="hidden" id="node-input-outputs" value="1">
<label></label>
<select id="node-input-drop-select" style="width: 70%">
<option id="node-input-drop-select-queue" value="queue" data-i18n="delay.queuemsg"></option>
<option value="drop" data-i18n="delay.dropmsg"></option>
<option value="emit" data-i18n="delay.sendmsg"></option>
</select>
</div>
<div class="form-row" id="rate-details-per-topic">
<label></label>
<select id="node-input-rate-topic-type" style="width:270px !important">
<option value="queue" data-i18n="delay.fairqueue"></option>
<option value="timed" data-i18n="delay.timedqueue"></option>
</select>
</div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('delay',{
category: 'function',
color:"#E6E0F8",
defaults: {
name: {value:""},
pauseType: {value:"delay", required:true},
timeout: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
timeoutUnits: {value:"seconds"},
rate: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
nbRateUnits: {value:"1", required:false,
validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
rateUnits: {value: "second"},
randomFirst: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomLast: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomUnits: {value: "seconds"},
drop: {value:false},
allowrate: {value:false},
outputs: { value: 1},
},
inputs:1,
outputs:1,
icon: "timer.svg",
label: function() {
if (this.name) {
return this.name;
}
if (this.pauseType == "delayv") {
return this._("delay.label.variable");
} else if (this.pauseType == "delay") {
var units = this.timeoutUnits ? this.timeoutUnits.charAt(0) : "s";
if (this.timeoutUnits == "milliseconds") { units = "ms"; }
return this._("delay.label.delay")+" "+this.timeout+units;
} else if (this.pauseType == "random") {
return this._("delay.label.random");
} else {
var rate = this.rate+" msg/"+(this.rateUnits ? (this.nbRateUnits > 1 ? this.nbRateUnits : '') + this.rateUnits.charAt(0) : "s");
if (this.pauseType == "rate") {
return this._("delay.label.limit")+" "+rate;
} else if (this.pauseType == "timed") {
return this._("delay.label.limitTopic")+" "+rate;
} else {
return this._("delay.label.limitTopic")+" "+rate;
}
}
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var node = this;
$( "#node-input-timeout" ).spinner({min:1});
$( "#node-input-rate" ).spinner({min:1});
$( "#node-input-nbRateUnits" ).spinner({min:1});
$( "#node-input-randomFirst" ).spinner({min:0});
$( "#node-input-randomLast" ).spinner({min:1});
$('.ui-spinner-button').on("click", function() {
$(this).siblings('input').trigger("change");
});
$( "#node-input-nbRateUnits" ).on('change keyup', function() {
var $this = $(this);
var val = parseInt($this.val());
var type = "singular";
if (val > 1) {
type = "plural";
}
if ($this.attr("data-type") == type) {
return;
}
$this.attr("data-type", type);
$("#node-input-rateUnits option").each(function () {
var $option = $(this);
var key = "delay.label.units." + $option.val() + "." + type;
$option.attr('data-i18n', 'node-red:' + key);
$option.html(node._(key));
});
});
if (this.pauseType == "delay") {
$("#node-input-delay-action").val('delay');
$("#node-input-delay-type").val('delay');
} else if (this.pauseType == "delayv") {
$("#node-input-delay-action").val('delay');
$("#node-input-delay-type").val('delayv');
} else if (this.pauseType == "random") {
$("#node-input-delay-action").val('delay');
$("#node-input-delay-type").val('random');
} else if (this.pauseType == "rate") {
$("#node-input-delay-action").val('rate');
$("#node-input-rate-type").val('all');
} else if (this.pauseType == "queue") {
$("#node-input-delay-action").val('rate');
$("#node-input-rate-type").val('topic');
$("#node-input-rate-topic-type").val('queue');
} else if (this.pauseType == "timed") {
$("#node-input-delay-action").val('rate');
$("#node-input-rate-type").val('topic');
$("#node-input-rate-topic-type").val('timed');
}
if (!this.timeoutUnits) {
$("#node-input-timeoutUnits option").filter(function() {
return $(this).val() == 'seconds';
}).attr('selected', true);
}
if (!this.randomUnits) {
$("#node-input-randomUnits option").filter(function() {
return $(this).val() == 'seconds';
}).attr('selected', true);
}
$("#node-input-delay-action").on("change",function() {
if (this.value === "delay") {
$("#delay-details").show();
$("#rate-details").hide();
} else if (this.value === "rate") {
$("#delay-details").hide();
$("#rate-details").show();
}
}).trigger("change");
$("#node-input-delay-type").on("change", function() {
if (this.value === "delay") {
$("#delay-details-for").show();
$("#random-details").hide();
} else if (this.value === "delayv") {
$("#delay-details-for").show();
$("#random-details").hide();
} else if (this.value === "random") {
$("#delay-details-for").hide();
$("#random-details").show();
}
}).trigger("change");
if (this.outputs === 2) {
$("#node-input-drop-select").val("emit");
} else if (this.drop) {
$("#node-input-drop-select").val("drop");
} else {
$("#node-input-drop-select").val("queue");
}
$("#node-input-rate-type").on("change", function() {
if (this.value === "all") {
$("#rate-details-per-topic").hide();
$("#node-input-drop-select-queue").attr('disabled', false);
} else if (this.value === "topic") {
if ($("#node-input-drop-select").val() === "queue") {
$("#node-input-drop-select").val("drop");
}
$("#node-input-drop-select-queue").attr('disabled', true);
$("#rate-details-per-topic").show();
}
}).trigger("change");
},
oneditsave: function() {
var action = $("#node-input-delay-action").val();
if (action === "delay") {
this.pauseType = $("#node-input-delay-type").val();
$("#node-input-outputs").val(1);
} else if (action === "rate") {
action = $("#node-input-rate-type").val();
if (action === "all") {
this.pauseType = "rate";
} else {
this.pauseType = $("#node-input-rate-topic-type").val();
}
var dropType = $("#node-input-drop-select").val();
this.drop = dropType !== "queue";
$("#node-input-outputs").val(dropType === "emit"?2:1);
}
}
});
</script>

View File

@ -1,200 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
var spawn = require('child_process').spawn;
var exec = require('child_process').exec;
var fs = require('fs');
var isUtf8 = require('is-utf8');
function ExecNode(n) {
RED.nodes.createNode(this,n);
this.cmd = (n.command || "").trim();
if (n.addpay === undefined) { n.addpay = true; }
this.addpay = n.addpay;
if (this.addpay === true) {
this.addpay = "payload";
}
this.append = (n.append || "").trim();
this.useSpawn = (n.useSpawn == "true");
this.timer = Number(n.timer || 0)*1000;
this.activeProcesses = {};
this.oldrc = (n.oldrc || false).toString();
this.execOpt = {encoding:'binary', maxBuffer:RED.settings.execMaxBufferSize||10000000, windowsHide: (n.winHide === true)};
this.spawnOpt = {windowsHide: (n.winHide === true) }
var node = this;
if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; }
var cleanup = function(p) {
node.activeProcesses[p].kill();
//node.status({fill:"red",shape:"dot",text:"timeout"});
//node.error("Exec node timeout");
}
this.on("input", function(msg, nodeSend, nodeDone) {
if (msg.hasOwnProperty("kill")) {
if (typeof msg.kill !== "string" || msg.kill.length === 0 || !msg.kill.toUpperCase().startsWith("SIG") ) { msg.kill = "SIGTERM"; }
if (msg.hasOwnProperty("pid")) {
if (node.activeProcesses.hasOwnProperty(msg.pid) ) {
node.activeProcesses[msg.pid].kill(msg.kill.toUpperCase());
node.status({fill:"red",shape:"dot",text:"killed"});
}
}
else {
if (Object.keys(node.activeProcesses).length === 1) {
node.activeProcesses[Object.keys(node.activeProcesses)[0]].kill(msg.kill.toUpperCase());
node.status({fill:"red",shape:"dot",text:"killed"});
}
}
nodeDone();
}
else {
var child;
// make the extra args into an array
// then prepend with the msg.payload
var arg = node.cmd;
if (node.addpay) {
var value = RED.util.getMessageProperty(msg, node.addpay);
if (value !== undefined) {
arg += " " + value;
}
}
if (node.append.trim() !== "") { arg += " " + node.append; }
if (this.useSpawn === true) {
// slice whole line by spaces and removes any quotes since spawn can't handle them
arg = arg.match(/(?:[^\s"]+|"[^"]*")+/g).map((a) => {
if (/^".*"$/.test(a)) {
return a.slice(1,-1)
} else {
return a
}
});
var cmd = arg.shift();
/* istanbul ignore else */
node.debug(cmd+" ["+arg+"]");
child = spawn(cmd,arg,node.spawnOpt);
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
var unknownCommand = (child.pid === undefined);
if (node.timer !== 0) {
child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer);
}
node.activeProcesses[child.pid] = child;
child.stdout.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
// console.log('[exec] stdout: ' + data,child.pid);
if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = data; }
nodeSend([RED.util.cloneMessage(msg),null,null]);
}
});
child.stderr.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = Buffer.from(data); }
nodeSend([null,RED.util.cloneMessage(msg),null]);
}
});
child.on('close', function (code,signal) {
if (unknownCommand || (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null)) {
delete node.activeProcesses[child.pid];
if (child.tout) { clearTimeout(child.tout); }
msg.payload = code;
if (node.oldrc === "false") {
msg.payload = {code:code};
if (signal) { msg.payload.signal = signal; }
}
if (code === 0) { node.status({}); }
if (code === null) { node.status({fill:"red",shape:"dot",text:"killed"}); }
else if (code < 0) { node.status({fill:"red",shape:"dot",text:"rc:"+code}); }
else { node.status({fill:"yellow",shape:"dot",text:"rc:"+code}); }
nodeSend([null,null,RED.util.cloneMessage(msg)]);
}
nodeDone();
});
child.on('error', function (code) {
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
node.error(code,RED.util.cloneMessage(msg));
}
});
}
else {
/* istanbul ignore else */
node.debug(arg);
child = exec(arg, node.execOpt, function (error, stdout, stderr) {
var msg2, msg3;
delete msg.payload;
if (stderr) {
msg2 = RED.util.cloneMessage(msg);
msg2.payload = stderr;
}
msg.payload = Buffer.from(stdout,"binary");
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }
node.status({});
//console.log('[exec] stdout: ' + stdout);
//console.log('[exec] stderr: ' + stderr);
if (error !== null) {
msg3 = RED.util.cloneMessage(msg);
msg3.payload = {code:error.code, message:error.message};
if (error.signal) { msg3.payload.signal = error.signal; }
if (error.code === null) { node.status({fill:"red",shape:"dot",text:"killed"}); }
else { node.status({fill:"red",shape:"dot",text:"error:"+error.code}); }
node.debug('error:' + error);
}
else if (node.oldrc === "false") {
msg3 = RED.util.cloneMessage(msg);
msg3.payload = {code:0};
}
if (!msg3) { node.status({}); }
else {
msg.rc = msg3.payload;
if (msg2) { msg2.rc = msg3.payload; }
}
nodeSend([msg,msg2,msg3]);
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];
nodeDone();
});
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
child.on('error',function() {});
if (node.timer !== 0) {
child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer);
}
node.activeProcesses[child.pid] = child;
}
}
});
this.on('close',function() {
for (var pid in node.activeProcesses) {
/* istanbul ignore else */
if (node.activeProcesses.hasOwnProperty(pid)) {
if (node.activeProcesses[pid].tout) { clearTimeout(node.activeProcesses[pid].tout); }
// console.log("KILLING",pid);
var process = node.activeProcesses[pid];
node.activeProcesses[pid] = null;
process.kill();
}
}
node.activeProcesses = {};
node.status({});
});
}
RED.nodes.registerType("exec",ExecNode);
}

View File

@ -1,97 +0,0 @@
module.exports = function(RED) {
"use strict";
function RbeNode(n) {
RED.nodes.createNode(this,n);
this.func = n.func || "rbe";
this.gap = n.gap || "0";
this.start = n.start || '';
this.inout = n.inout || "out";
this.pc = false;
if (this.gap.substr(-1) === "%") {
this.pc = true;
this.gap = parseFloat(this.gap);
}
this.g = this.gap;
this.property = n.property || "payload";
this.topi = n.topi || "topic";
this.septopics = true;
if (n.septopics !== undefined && n.septopics === false) {
this.septopics = false;
}
var node = this;
node.previous = {};
this.on("input",function(msg) {
var topic;
try {
topic = RED.util.getMessageProperty(msg,node.topi);
}
catch(e) { }
if (msg.hasOwnProperty("reset")) {
if (node.septopics && topic && (typeof topic === "string") && (topic !== "")) {
delete node.previous[msg.topic];
}
else { node.previous = {}; }
}
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
var t = "_no_topic";
if (node.septopics) { t = topic || t; }
if ((this.func === "rbe") || (this.func === "rbei")) {
var doSend = (this.func !== "rbei") || (node.previous.hasOwnProperty(t)) || false;
if (typeof(value) === "object") {
if (typeof(node.previous[t]) !== "object") { node.previous[t] = {}; }
if (!RED.util.compareObjects(value, node.previous[t])) {
node.previous[t] = RED.util.cloneMessage(value);
if (doSend) { node.send(msg); }
}
}
else {
if (value !== node.previous[t]) {
node.previous[t] = RED.util.cloneMessage(value);
if (doSend) { node.send(msg); }
}
}
}
else {
var n = parseFloat(value);
if (!isNaN(n)) {
if ((typeof node.previous[t] === 'undefined') && (this.func === "narrowband" || this.func === "narrowbandEq")) {
if (node.start === '') { node.previous[t] = n; }
else { node.previous[t] = node.start; }
}
if (node.pc) { node.gap = Math.abs(node.previous[t] * node.g / 100) || 0; }
else { node.gap = Number(node.gap); }
if ((node.previous[t] === undefined) && (node.func === "narrowbandEq")) { node.previous[t] = n; }
if (node.previous[t] === undefined) { node.previous[t] = n - node.gap - 1; }
if (Math.abs(n - node.previous[t]) === node.gap) {
if ((this.func === "deadbandEq")||(this.func === "narrowband")) {
if (node.inout === "out") { node.previous[t] = n; }
node.send(msg);
}
}
else if (Math.abs(n - node.previous[t]) > node.gap) {
if (this.func === "deadband" || this.func === "deadbandEq") {
if (node.inout === "out") { node.previous[t] = n; }
node.send(msg);
}
}
else if (Math.abs(n - node.previous[t]) < node.gap) {
if ((this.func === "narrowband")||(this.func === "narrowbandEq")) {
if (node.inout === "out") { node.previous[t] = n; }
node.send(msg);
}
}
if (node.inout === "in") { node.previous[t] = n; }
}
else {
node.warn(RED._("rbe.warn.nonumber"));
}
}
} // ignore msg with no payload property.
});
}
RED.nodes.registerType("rbe",RbeNode);
}

View File

@ -1,675 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
const got = require("got");
const {CookieJar} = require("tough-cookie");
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
const FormData = require('form-data');
const { v4: uuid } = require('uuid');
const crypto = require('crypto');
const URL = require("url").URL
var mustache = require("mustache");
var querystring = require("querystring");
var cookie = require("cookie");
var hashSum = require("hash-sum");
// Cache a reference to the existing https.request function
// so we can compare later to see if an old agent-base instance
// has been required.
// This is generally okay as the core nodes are required before
// any contrib nodes. Where it will fail is if the agent-base module
// is required via the settings file or outside of Node-RED before it
// is started.
// If there are other modules that patch the function, they will get undone
// as well. Not much we can do about that right now. Patching core
// functions is bad.
const HTTPS_MODULE = require("https");
const HTTPS_REQUEST = HTTPS_MODULE.request;
function checkNodeAgentPatch() {
if (HTTPS_MODULE.request !== HTTPS_REQUEST && HTTPS_MODULE.request.length === 2) {
RED.log.warn(`
---------------------------------------------------------------------
Patched https.request function detected. This will break the
HTTP Request node. The original code has now been restored.
This is likely caused by a contrib node including an old version of
the 'agent-base@<5.0.0' module.
You can identify what node is at fault by running:
npm list agent-base
in your Node-RED user directory (${RED.settings.userDir}).
---------------------------------------------------------------------
`);
HTTPS_MODULE.request = HTTPS_REQUEST
}
}
function HTTPRequest(n) {
RED.nodes.createNode(this,n);
checkNodeAgentPatch();
var node = this;
var nodeUrl = n.url;
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
var nodeMethod = n.method || "GET";
var paytoqs = false;
var paytobody = false;
var redirectList = [];
var sendErrorsToCatch = n.senderr;
var nodeHTTPPersistent = n["persist"];
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
this.ret = n.ret || "txt";
this.authType = n.authType || "basic";
if (RED.settings.httpRequestTimeout) { this.reqTimeout = parseInt(RED.settings.httpRequestTimeout) || 120000; }
else { this.reqTimeout = 120000; }
if (n.paytoqs === true || n.paytoqs === "query") { paytoqs = true; }
else if (n.paytoqs === "body") { paytobody = true; }
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
var proxyConfig = null;
if (n.proxy) {
proxyConfig = RED.nodes.getNode(n.proxy);
prox = proxyConfig.url;
noprox = proxyConfig.noproxy;
}
let timingLog = false;
if (RED.settings.hasOwnProperty("httpRequestTimingLog")) {
timingLog = RED.settings.httpRequestTimingLog;
}
this.on("input",function(msg,nodeSend,nodeDone) {
checkNodeAgentPatch();
//reset redirectList on each request
redirectList = [];
var preRequestTimestamp = process.hrtime();
node.status({fill:"blue",shape:"dot",text:"httpin.status.requesting"});
var url = nodeUrl || msg.url;
if (msg.url && nodeUrl && (nodeUrl !== msg.url)) { // revert change below when warning is finally removed
node.warn(RED._("common.errors.nooverride"));
}
if (isTemplatedUrl) {
url = mustache.render(nodeUrl,msg);
}
if (!url) {
node.error(RED._("httpin.errors.no-url"),msg);
nodeDone();
return;
}
// url must start http:// or https:// so assume http:// if not set
if (url.indexOf("://") !== -1 && url.indexOf("http") !== 0) {
node.warn(RED._("httpin.errors.invalid-transport"));
node.status({fill:"red",shape:"ring",text:"httpin.errors.invalid-transport"});
nodeDone();
return;
}
if (!((url.indexOf("http://") === 0) || (url.indexOf("https://") === 0))) {
if (tlsNode) {
url = "https://"+url;
} else {
url = "http://"+url;
}
}
// The Request module used in Node-RED 1.x was tolerant of query strings that
// were partially encoded. For example - "?a=hello%20there&b=20%"
// The GOT module doesn't like that.
// The following is an attempt to normalise the url to ensure it is properly
// encoded. We cannot just encode it directly as we don't want any valid
// encoded entity to end up doubly encoded.
if (url.indexOf("?") > -1) {
// Only do this if there is a query string to deal with
const [hostPath, ...queryString] = url.split("?")
const query = queryString.join("?");
if (query) {
// Look for any instance of % not followed by two hex chars.
// Replace any we find with %25.
const escapedQueryString = query.replace(/(%.?.?)/g, function(v) {
if (/^%[a-f0-9]{2}/i.test(v)) {
return v;
}
return v.replace(/%/,"%25")
})
url = hostPath+"?"+escapedQueryString;
}
}
var method = nodeMethod.toUpperCase() || "GET";
if (msg.method && n.method && (n.method !== "use")) { // warn if override option not set
node.warn(RED._("common.errors.nooverride"));
}
if (msg.method && n.method && (n.method === "use")) {
method = msg.method.toUpperCase(); // use the msg parameter
}
// var isHttps = (/^https/i.test(url));
var opts = {};
// set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(.
// Had to remove this to get http->https redirect to work
// opts.defaultPort = isHttps?443:80;
opts.timeout = node.reqTimeout;
opts.throwHttpErrors = false;
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
opts.decompress = false;
opts.method = method;
opts.headers = {};
opts.retry = 0;
opts.responseType = 'buffer';
opts.maxRedirects = 21;
opts.cookieJar = new CookieJar();
opts.ignoreInvalidCookies = true;
opts.forever = nodeHTTPPersistent;
if (msg.requestTimeout !== undefined) {
if (isNaN(msg.requestTimeout)) {
node.warn(RED._("httpin.errors.timeout-isnan"));
} else if (msg.requestTimeout < 1) {
node.warn(RED._("httpin.errors.timeout-isnegative"));
} else {
opts.timeout = msg.requestTimeout;
}
}
const originalHeaderMap = {};
opts.hooks = {
beforeRequest: [
options => {
// Whilst HTTP headers are meant to be case-insensitive,
// in the real world, there are servers that aren't so compliant.
// GOT will lower case all headers given a chance, so we need
// to restore the case of any headers the user has set.
Object.keys(options.headers).forEach(h => {
if (originalHeaderMap[h] && originalHeaderMap[h] !== h) {
options.headers[originalHeaderMap[h]] = options.headers[h];
delete options.headers[h];
}
})
}
],
beforeRedirect: [
(options, response) => {
let redirectInfo = {
location: response.headers.location
}
if (response.headers.hasOwnProperty('set-cookie')) {
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
}
redirectList.push(redirectInfo)
}
]
}
var ctSet = "Content-Type"; // set default camel case
var clSet = "Content-Length";
if (msg.headers) {
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
var headerHash = msg.headers['x-node-red-request-node'];
delete msg.headers['x-node-red-request-node'];
var hash = hashSum(msg.headers);
if (hash === headerHash) {
delete msg.headers;
}
}
if (msg.headers) {
for (var v in msg.headers) {
if (msg.headers.hasOwnProperty(v)) {
var name = v.toLowerCase();
if (name !== "content-type" && name !== "content-length") {
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
name = v;
}
else if (name === 'content-type') { ctSet = v; }
else { clSet = v; }
opts.headers[name] = msg.headers[v];
}
}
}
}
if (msg.hasOwnProperty('followRedirects')) {
opts.followRedirect = !!msg.followRedirects;
}
if (opts.headers.hasOwnProperty('cookie')) {
var cookies = cookie.parse(opts.headers.cookie, {decode:String});
for (var name in cookies) {
opts.cookieJar.setCookieSync(cookie.serialize(name, cookies[name], {encode:String}), url, {ignoreError: true});
}
delete opts.headers.cookie;
}
if (msg.cookies) {
for (var name in msg.cookies) {
if (msg.cookies.hasOwnProperty(name)) {
if (msg.cookies[name] === null || msg.cookies[name].value === null) {
// This case clears a cookie for HTTP In/Response nodes.
// Ignore for this node.
} else if (typeof msg.cookies[name] === 'object') {
if(msg.cookies[name].encode === false){
// If the encode option is false, the value is not encoded.
opts.cookieJar.setCookieSync(cookie.serialize(name, msg.cookies[name].value, {encode: String}), url, {ignoreError: true});
} else {
// The value is encoded by encodeURIComponent().
opts.cookieJar.setCookieSync(cookie.serialize(name, msg.cookies[name].value), url, {ignoreError: true});
}
} else {
opts.cookieJar.setCookieSync(cookie.serialize(name, msg.cookies[name]), url, {ignoreError: true});
}
}
}
}
var parsedURL = new URL(url)
this.credentials = this.credentials || {}
if (parsedURL.username && !this.credentials.user) {
this.credentials.user = parsedURL.username
}
if (parsedURL.password && !this.credentials.password) {
this.credentials.password = parsedURL.password
}
if (Object.keys(this.credentials).length != 0) {
if (this.authType === "basic") {
// Workaround for https://github.com/sindresorhus/got/issues/1169 (fixed in got v12)
// var cred = ""
if (this.credentials.user || this.credentials.password) {
// cred = `${this.credentials.user}:${this.credentials.password}`;
if (this.credentials.user === undefined) { this.credentials.user = ""}
if (this.credentials.password === undefined) { this.credentials.password = ""}
opts.headers.Authorization = "Basic " + Buffer.from(`${this.credentials.user}:${this.credentials.password}`).toString("base64");
}
// build own basic auth header
// opts.headers.Authorization = "Basic " + Buffer.from(cred).toString("base64");
} else if (this.authType === "digest") {
let digestCreds = this.credentials;
let sentCreds = false;
opts.hooks.afterResponse = [(response, retry) => {
if (response.statusCode === 401) {
if (sentCreds) {
return response
}
const requestUrl = new URL(response.request.requestUrl);
const options = response.request.options;
const normalisedHeaders = {};
Object.keys(response.headers).forEach(k => {
normalisedHeaders[k.toLowerCase()] = response.headers[k]
})
if (normalisedHeaders['www-authenticate']) {
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
options.headers.Authorization = authHeader;
}
sentCreds = true;
return retry(options);
}
return response
}];
} else if (this.authType === "bearer") {
opts.headers.Authorization = `Bearer ${this.credentials.password||""}`
}
}
var payload = null;
if (method !== 'GET' && method !== 'HEAD' && typeof msg.payload !== "undefined") {
if (opts.headers['content-type'] == 'multipart/form-data' && typeof msg.payload === "object") {
let formData = new FormData();
for (var opt in msg.payload) {
if (msg.payload.hasOwnProperty(opt)) {
var val = msg.payload[opt];
if (val !== undefined && val !== null) {
if (typeof val === 'string' || Buffer.isBuffer(val)) {
formData.append(opt, val);
} else if (typeof val === 'object' && val.hasOwnProperty('value')) {
formData.append(opt,val.value,val.options || {});
} else {
formData.append(opt,JSON.stringify(val));
}
}
}
}
// GOT will only set the content-type header with the correct boundary
// if the header isn't set. So we delete it here, for GOT to reset it.
delete opts.headers['content-type'];
opts.body = formData;
} else {
if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
payload = msg.payload;
} else if (typeof msg.payload == "number") {
payload = msg.payload+"";
} else {
if (opts.headers['content-type'] == 'application/x-www-form-urlencoded') {
payload = querystring.stringify(msg.payload);
} else {
payload = JSON.stringify(msg.payload);
if (opts.headers['content-type'] == null) {
opts.headers[ctSet] = "application/json";
}
}
}
if (opts.headers['content-length'] == null) {
if (Buffer.isBuffer(payload)) {
opts.headers[clSet] = payload.length;
} else {
opts.headers[clSet] = Buffer.byteLength(payload);
}
}
opts.body = payload;
}
}
if (method == 'GET' && typeof msg.payload !== "undefined" && paytoqs) {
if (typeof msg.payload === "object") {
try {
if (url.indexOf("?") !== -1) {
url += (url.endsWith("?")?"":"&") + querystring.stringify(msg.payload);
} else {
url += "?" + querystring.stringify(msg.payload);
}
} catch(err) {
node.error(RED._("httpin.errors.invalid-payload"),msg);
nodeDone();
return;
}
} else {
node.error(RED._("httpin.errors.invalid-payload"),msg);
nodeDone();
return;
}
} else if ( method == "GET" && typeof msg.payload !== "undefined" && paytobody) {
opts.allowGetBody = true;
if (typeof msg.payload === "object") {
opts.body = JSON.stringify(msg.payload);
} else if (typeof msg.payload == "number") {
opts.body = msg.payload+"";
} else if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
opts.body = msg.payload;
}
}
// revert to user supplied Capitalisation if needed.
if (opts.headers.hasOwnProperty('content-type') && (ctSet !== 'content-type')) {
opts.headers[ctSet] = opts.headers['content-type'];
delete opts.headers['content-type'];
}
if (opts.headers.hasOwnProperty('content-length') && (clSet !== 'content-length')) {
opts.headers[clSet] = opts.headers['content-length'];
delete opts.headers['content-length'];
}
var noproxy;
if (noprox) {
for (var i = 0; i < noprox.length; i += 1) {
if (url.indexOf(noprox[i]) !== -1) { noproxy=true; }
}
}
if (prox && !noproxy) {
var match = prox.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i);
if (match) {
let proxyAgent;
let proxyURL = new URL(prox);
//set username/password to null to stop empty creds header
let proxyOptions = {
proxy: {
protocol: proxyURL.protocol,
hostname: proxyURL.hostname,
port: proxyURL.port,
username: null,
password: null
},
maxFreeSockets: 256,
maxSockets: 256,
keepAlive: true
}
if (proxyConfig && proxyConfig.credentials) {
let proxyUsername = proxyConfig.credentials.username || '';
let proxyPassword = proxyConfig.credentials.password || '';
if (proxyUsername || proxyPassword) {
proxyOptions.proxy.username = proxyUsername;
proxyOptions.proxy.password = proxyPassword;
}
} else if (proxyURL.username || proxyURL.password){
proxyOptions.proxy.username = proxyURL.username;
proxyOptions.proxy.password = proxyURL.password;
}
//need both incase of http -> https redirect
opts.agent = {
http: new HttpProxyAgent(proxyOptions),
https: new HttpsProxyAgent(proxyOptions)
};
} else {
node.warn("Bad proxy url: "+ prox);
}
}
if (tlsNode) {
opts.https = {};
tlsNode.addTLSOptions(opts.https);
if (opts.https.ca) {
opts.https.certificateAuthority = opts.https.ca;
delete opts.https.ca;
}
if (opts.https.cert) {
opts.https.certificate = opts.https.cert;
delete opts.https.cert;
}
} else {
if (msg.hasOwnProperty('rejectUnauthorized')) {
opts.https = { rejectUnauthorized: msg.rejectUnauthorized };
}
}
// Now we have established all of our own headers, take a snapshot
// of their case so we can restore it prior to the request being sent.
if (opts.headers) {
Object.keys(opts.headers).forEach(h => {
originalHeaderMap[h.toLowerCase()] = h
})
}
got(url,opts).then(res => {
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.responseUrl = res.url;
msg.payload = res.body;
msg.redirectList = redirectList;
msg.retry = 0;
if (msg.headers.hasOwnProperty('set-cookie')) {
msg.responseCookies = extractCookies(msg.headers['set-cookie']);
}
msg.headers['x-node-red-request-node'] = hashSum(msg.headers);
// msg.url = url; // revert when warning above finally removed
if (node.metric()) {
// Calculate request time
var diff = process.hrtime(preRequestTimestamp);
var ms = diff[0] * 1e3 + diff[1] * 1e-6;
var metricRequestDurationMillis = ms.toFixed(3);
node.metric("duration.millis", msg, metricRequestDurationMillis);
if (res.client && res.client.bytesRead) {
node.metric("size.bytes", msg, res.client.bytesRead);
}
if (timingLog) {
emitTimingMetricLog(res.timings, msg);
}
}
// Convert the payload to the required return type
if (node.ret !== "bin") {
msg.payload = msg.payload.toString('utf8'); // txt
if (node.ret === "obj") {
try { msg.payload = JSON.parse(msg.payload); } // obj
catch(e) { node.warn(RED._("httpin.errors.json-error")); }
}
}
node.status({});
nodeSend(msg);
nodeDone();
}).catch(err => {
// Pre 2.1, any errors would be sent to both Catch node and sent on as normal.
// This is not ideal but is the legacy behaviour of the node.
// 2.1 adds the 'senderr' option, if set to true, will *only* send errors
// to Catch nodes. If false, it still does both behaviours.
// TODO: 3.0 - make it one or the other.
if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
node.error(RED._("common.notification.errors.no-response"), msg);
node.status({fill:"red", shape:"ring", text:"common.notification.errors.no-response"});
} else {
node.error(err,msg);
node.status({fill:"red", shape:"ring", text:err.code});
}
msg.payload = err.toString() + " : " + url;
msg.statusCode = err.code || (err.response?err.response.statusCode:undefined);
if (node.metric() && timingLog) {
emitTimingMetricLog(err.timings, msg);
}
if (!sendErrorsToCatch) {
nodeSend(msg);
}
nodeDone();
});
});
this.on("close",function() {
node.status({});
});
function emitTimingMetricLog(timings, msg) {
const props = [
"start",
"socket",
"lookup",
"connect",
"secureConnect",
"upload",
"response",
"end",
"error",
"abort"
];
if (timings) {
props.forEach(p => {
if (timings[p]) {
node.metric(`timings.${p}`, msg, timings[p]);
}
});
}
}
function extractCookies(setCookie) {
var cookies = {};
setCookie.forEach(function(c) {
var parsedCookie = cookie.parse(c);
var eq_idx = c.indexOf('=');
var key = c.substr(0, eq_idx).trim()
parsedCookie.value = parsedCookie[key];
delete parsedCookie[key];
cookies[key] = parsedCookie;
});
return cookies;
}
}
RED.nodes.registerType("http request",HTTPRequest,{
credentials: {
user: {type:"text"},
password: {type: "password"}
}
});
const md5 = (value) => { return crypto.createHash('md5').update(value).digest('hex') }
function ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
/**
* RFC 2617: handle both MD5 and MD5-sess algorithms.
*
* If the algorithm directive's value is "MD5" or unspecified, then HA1 is
* HA1=MD5(username:realm:password)
* If the algorithm directive's value is "MD5-sess", then HA1 is
* HA1=MD5(MD5(username:realm:password):nonce:cnonce)
*/
var ha1 = md5(user + ':' + realm + ':' + pass)
if (algorithm && algorithm.toLowerCase() === 'md5-sess') {
return md5(ha1 + ':' + nonce + ':' + cnonce)
} else {
return ha1
}
}
function buildDigestHeader(user, pass, method, path, authHeader) {
var challenge = {}
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
for (;;) {
var match = re.exec(authHeader)
if (!match) {
break
}
challenge[match[1]] = match[2] || match[3]
}
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
var nc = qop && '00000001'
var cnonce = qop && uuid().replace(/-/g, '')
var ha1 = ha1Compute(challenge.algorithm, user, challenge.realm, pass, challenge.nonce, cnonce)
var ha2 = md5(method + ':' + path)
var digestResponse = qop
? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
: md5(ha1 + ':' + challenge.nonce + ':' + ha2)
var authValues = {
username: user,
realm: challenge.realm,
nonce: challenge.nonce,
uri: path,
qop: qop,
response: digestResponse,
nc: nc,
cnonce: cnonce,
algorithm: challenge.algorithm,
opaque: challenge.opaque
}
authHeader = []
for (var k in authValues) {
if (authValues[k]) {
if (k === 'qop' || k === 'nc' || k === 'algorithm') {
authHeader.push(k + '=' + authValues[k])
} else {
authHeader.push(k + '="' + authValues[k] + '"')
}
}
}
authHeader = 'Digest ' + authHeader.join(', ')
return authHeader
}
}

View File

@ -1,292 +0,0 @@
<!--
Copyright JS Foundation and other contributors, http://js.foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- WebSocket Input Node -->
<script type="text/html" data-template-name="websocket in">
<div class="form-row">
<label for="node-input-mode"><i class="fa fa-dot-circle-o"></i> <span data-i18n="websocket.label.type"></span></label>
<select id="node-input-mode">
<option value="server" data-i18n="websocket.listenon"></option>
<option value="client" data-i18n="websocket.connectto"></option>
</select>
</div>
<div class="form-row" id="websocket-server-row">
<label for="node-input-server"><i class="fa fa-bookmark"></i> <span data-i18n="websocket.label.path"></span></label>
<input type="text" id="node-input-server">
</div>
<div class="form-row" id="websocket-client-row">
<label for="node-input-client"><i class="fa fa-bookmark"></i> <span data-i18n="websocket.label.url"></span></label>
<input type="text" id="node-input-client">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
(function() {
function ws_oneditprepare() {
$("#websocket-client-row").hide();
$("#node-input-mode").on("change", function() {
if ( $("#node-input-mode").val() === 'client') {
$("#websocket-server-row").hide();
$("#websocket-client-row").show();
}
else {
$("#websocket-server-row").show();
$("#websocket-client-row").hide();
}
});
if (this.client) {
$("#node-input-mode").val('client').change();
}
else {
$("#node-input-mode").val('server').change();
}
}
function ws_oneditsave() {
if ($("#node-input-mode").val() === 'client') {
$("#node-input-server").append('<option value="">Dummy</option>');
$("#node-input-server").val('');
}
else {
$("#node-input-client").append('<option value="">Dummy</option>');
$("#node-input-client").val('');
}
}
function ws_label() {
var nodeid = (this.client)?this.client:this.server;
var wsNode = RED.nodes.node(nodeid);
return this.name||(wsNode?"[ws] "+wsNode.label():"websocket");
}
function ws_validateserver() {
if ($("#node-input-mode").val() === 'client' || (this.client && !this.server)) {
return true;
}
else {
return RED.nodes.node(this.server) != null;
}
}
function ws_validateclient() {
if ($("#node-input-mode").val() === 'client' || (this.client && !this.server)) {
return RED.nodes.node(this.client) != null;
}
else {
return true;
}
}
RED.nodes.registerType('websocket in',{
category: 'network',
defaults: {
name: {value:""},
server: {type:"websocket-listener", validate: ws_validateserver},
client: {type:"websocket-client", validate: ws_validateclient}
},
color:"rgb(215, 215, 160)",
inputs:0,
outputs:1,
icon: "white-globe.svg",
labelStyle: function() {
return this.name?"node_label_italic":"";
},
label: ws_label,
oneditsave: ws_oneditsave,
oneditprepare: ws_oneditprepare
});
RED.nodes.registerType('websocket out',{
category: 'network',
defaults: {
name: {value:""},
server: {type:"websocket-listener", validate: ws_validateserver},
client: {type:"websocket-client", validate: ws_validateclient}
},
color:"rgb(215, 215, 160)",
inputs:1,
outputs:0,
icon: "white-globe.svg",
align: "right",
labelStyle: function() {
return this.name?"node_label_italic":"";
},
label: ws_label,
oneditsave: ws_oneditsave,
oneditprepare: ws_oneditprepare
});
RED.nodes.registerType('websocket-listener',{
category: 'config',
defaults: {
path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
wholemsg: {value:"false"}
},
inputs:0,
outputs:0,
label: function() {
var root = RED.settings.httpNodeRoot;
if (root.slice(-1) != "/") {
root = root+"/";
}
if (this.path) {
if (this.path.charAt(0) == "/") {
root += this.path.slice(1);
} else {
root += this.path;
}
}
return root;
},
oneditprepare: function() {
var root = RED.settings.httpNodeRoot;
if (root.slice(-1) == "/") {
root = root.slice(0,-1);
}
if (root === "") {
$("#node-config-ws-tip").hide();
} else {
$("#node-config-ws-path").html(RED._("node-red:websocket.tip.path2", { path: root }));
$("#node-config-ws-tip").show();
}
}
});
RED.nodes.registerType('websocket-client',{
category: 'config',
defaults: {
path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
tls: {type:"tls-config",required: false},
wholemsg: {value:"false"},
hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) },
subprotocol: {value:"",required: false}
},
inputs:0,
outputs:0,
label: function() {
return this.path;
},
oneditprepare: function() {
$("#node-config-input-path").on("change keyup paste",function() {
$(".node-config-row-tls").toggle(/^wss:/i.test($(this).val()))
});
$("#node-config-input-path").change();
var heartbeatActive = (this.hb && this.hb != "0");
$("#node-config-input-hb-cb").prop("checked",heartbeatActive);
$("#node-config-input-hb-cb").on("change", function(evt) {
$("#node-config-input-hb-row").toggle(this.checked);
})
$("#node-config-input-hb-cb").trigger("change");
if (!heartbeatActive) {
$("#node-config-input-hb").val("");
}
},
oneditsave: function() {
if (!/^wss:/i.test($("#node-config-input-path").val())) {
$("#node-config-input-tls").val("_ADD_");
}
if (!$("#node-config-input-hb-cb").prop("checked")) {
$("#node-config-input-hb").val("0");
}
}
});
})();
</script>
<!-- WebSocket out Node -->
<script type="text/html" data-template-name="websocket out">
<div class="form-row">
<label for="node-input-mode"><i class="fa fa-dot-circle-o"></i> <span data-i18n="websocket.label.type"></span></label>
<select id="node-input-mode">
<option value="server" data-i18n="websocket.listenon"></option>
<option value="client" data-i18n="websocket.connectto"></option>
</select>
</div>
<div class="form-row" id="websocket-server-row">
<label for="node-input-server"><i class="fa fa-bookmark"></i> <span data-i18n="websocket.label.path"></span></label>
<input type="text" id="node-input-server">
</div>
<div class="form-row" id="websocket-client-row">
<label for="node-input-client"><i class="fa fa-bookmark"></i> <span data-i18n="websocket.label.url"></span></label>
<input type="text" id="node-input-client">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<!-- WebSocket Server configuration node -->
<script type="text/html" data-template-name="websocket-listener">
<div class="form-row">
<label for="node-config-input-path"><i class="fa fa-bookmark"></i> <span data-i18n="websocket.label.path"></span></label>
<input id="node-config-input-path" type="text" placeholder="/ws/example">
</div>
<div class="form-row">
<label for="node-config-input-wholemsg" data-i18n="websocket.sendrec"></label>
<select type="text" id="node-config-input-wholemsg" style="width: 70%;">
<option value="false" data-i18n="websocket.payload"></option>
<option value="true" data-i18n="websocket.message"></option>
</select>
</div>
<div class="form-tips">
<span data-i18n="[html]websocket.tip.path1"></span>
<p id="node-config-ws-tip"><span id="node-config-ws-path"></span></p>
</div>
</script>
<!-- WebSocket Client configuration node -->
<script type="text/html" data-template-name="websocket-client">
<div class="form-row">
<label for="node-config-input-path"><i class="fa fa-bookmark"></i> <span data-i18n="websocket.label.url"></span></label>
<input id="node-config-input-path" type="text" placeholder="ws://example.com/ws">
</div>
<div class="form-row node-config-row-tls hide">
<label for="node-config-input-tls" data-i18n="httpin.tls-config"></label>
<input type="text" id="node-config-input-tls">
</div>
<div class="form-row">
<label for="node-config-input-subprotocol"><i class="fa fa-tag"></i> <span data-i18n="websocket.label.subprotocol"></span></label>
<input type="text" id="node-config-input-subprotocol">
</div>
<div class="form-row">
<label for="node-config-input-wholemsg" data-i18n="websocket.sendrec"></label>
<select type="text" id="node-config-input-wholemsg" style="width: 70%;">
<option value="false" data-i18n="websocket.payload"></option>
<option value="true" data-i18n="websocket.message"></option>
</select>
</div>
<div class="form-row" style="display: flex; align-items: center; min-height: 34px">
<label for="node-config-input-hb-cb" data-i18n="websocket.sendheartbeat"></label>
<input type="checkbox" style="margin: 0 8px; width:auto" id="node-config-input-hb-cb">
<span id="node-config-input-hb-row" class="hide" >
<input type="text" style="width: 70px; margin-right: 3px" id="node-config-input-hb">
<span data-i18n="inject.seconds"></span>
</span>
</div>
<div class="form-tips">
<p><span data-i18n="[html]websocket.tip.url1"></span></p>
<span data-i18n="[html]websocket.tip.url2"></span>
</div>
</script>

View File

@ -1,423 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
var ws = require("ws");
var inspect = require("util").inspect;
var url = require("url");
var HttpsProxyAgent = require('https-proxy-agent');
var serverUpgradeAdded = false;
function handleServerUpgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
if (listenerNodes.hasOwnProperty(pathname)) {
listenerNodes[pathname].server.handleUpgrade(request, socket, head, function done(ws) {
listenerNodes[pathname].server.emit('connection', ws, request);
});
} else {
// Don't destroy the socket as other listeners may want to handle the
// event.
}
}
var listenerNodes = {};
var activeListenerNodes = 0;
// A node red node that sets up a local websocket server
function WebSocketListenerNode(n) {
// Create a RED node
RED.nodes.createNode(this,n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
node.path = n.path;
if (typeof n.subprotocol === "string") {
// Split the string on comma and trim each result
node.subprotocol = n.subprotocol.split(",").map(v => v.trim())
} else {
node.subprotocol = [];
}
node.wholemsg = (n.wholemsg === "true");
node._inputNodes = []; // collection of nodes that want to receive events
node._clients = {};
// match absolute url
node.isServer = !/^ws{1,2}:\/\//i.test(node.path);
node.closing = false;
node.tls = n.tls;
if (n.hb) {
var heartbeat = parseInt(n.hb);
if (heartbeat > 0) {
node.heartbeat = heartbeat * 1000;
}
}
function startconn() { // Connect to remote endpoint
node.tout = null;
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
var noproxy = false;
if (noprox) {
for (var i in noprox) {
if (node.path.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
}
}
var agent = undefined;
if (prox && !noproxy) {
agent = new HttpsProxyAgent(prox);
}
var options = {};
if (agent) {
options.agent = agent;
}
if (node.tls) {
var tlsNode = RED.nodes.getNode(node.tls);
if (tlsNode) {
tlsNode.addTLSOptions(options);
}
}
var socket = new ws(node.path,node.subprotocol,options);
socket.setMaxListeners(0);
node.server = socket; // keep for closing
handleConnection(socket);
}
function handleConnection(/*socket*/socket) {
var id = RED.util.generateId();
socket.nrId = id;
socket.nrPendingHeartbeat = false;
if (node.isServer) {
node._clients[id] = socket;
node.emit('opened',{count:Object.keys(node._clients).length,id:id});
}
socket.on('open',function() {
if (!node.isServer) {
if (node.heartbeat) {
clearInterval(node.heartbeatInterval);
node.heartbeatInterval = setInterval(function() {
if (socket.nrPendingHeartbeat) {
// No pong received
socket.terminate();
socket.nrErrorHandler(new Error("timeout"));
return;
}
socket.nrPendingHeartbeat = true;
try {
socket.ping();
} catch(err) {}
},node.heartbeat);
}
node.emit('opened',{count:'',id:id});
}
});
socket.on('close',function() {
clearInterval(node.heartbeatInterval);
if (node.isServer) {
delete node._clients[id];
node.emit('closed',{count:Object.keys(node._clients).length,id:id});
} else {
node.emit('closed',{count:'',id:id});
}
if (!node.closing && !node.isServer) {
clearTimeout(node.tout);
node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ?
}
});
socket.on('message',function(data,flags) {
node.handleEvent(id,socket,'message',data,flags);
});
socket.nrErrorHandler = function(err) {
clearInterval(node.heartbeatInterval);
node.emit('erro',{err:err,id:id});
if (!node.closing && !node.isServer) {
clearTimeout(node.tout);
node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ?
}
}
socket.on('error',socket.nrErrorHandler);
socket.on('ping', function() {
socket.nrPendingHeartbeat = false;
})
socket.on('pong', function() {
socket.nrPendingHeartbeat = false;
})
}
if (node.isServer) {
activeListenerNodes++;
if (!serverUpgradeAdded) {
RED.server.on('upgrade', handleServerUpgrade);
serverUpgradeAdded = true
}
var path = RED.settings.httpNodeRoot || "/";
path = path + (path.slice(-1) == "/" ? "":"/") + (node.path.charAt(0) == "/" ? node.path.substring(1) : node.path);
node.fullPath = path;
if (listenerNodes.hasOwnProperty(path)) {
node.error(RED._("websocket.errors.duplicate-path",{path: node.path}));
return;
}
listenerNodes[node.fullPath] = node;
var serverOptions = {
noServer: true
}
if (RED.settings.webSocketNodeVerifyClient) {
serverOptions.verifyClient = RED.settings.webSocketNodeVerifyClient;
}
// Create a WebSocket Server
node.server = new ws.Server(serverOptions);
node.server.setMaxListeners(0);
node.server.on('connection', handleConnection);
// Not adding server-initiated heartbeats yet
// node.heartbeatInterval = setInterval(function() {
// node.server.clients.forEach(function(ws) {
// if (ws.nrPendingHeartbeat) {
// // No pong received
// ws.terminate();
// ws.nrErrorHandler(new Error("timeout"));
// return;
// }
// ws.nrPendingHeartbeat = true;
// ws.ping();
// });
// })
}
else {
node.closing = false;
startconn(); // start outbound connection
}
node.on("close", function() {
if (node.heartbeatInterval) {
clearInterval(node.heartbeatInterval);
}
if (node.isServer) {
delete listenerNodes[node.fullPath];
node.server.close();
node._inputNodes = [];
activeListenerNodes--;
// if (activeListenerNodes === 0 && serverUpgradeAdded) {
// RED.server.removeListener('upgrade', handleServerUpgrade);
// serverUpgradeAdded = false;
// }
}
else {
node.closing = true;
node.server.close();
if (node.tout) {
clearTimeout(node.tout);
node.tout = null;
}
}
});
}
RED.nodes.registerType("websocket-listener",WebSocketListenerNode);
RED.nodes.registerType("websocket-client",WebSocketListenerNode);
WebSocketListenerNode.prototype.registerInputNode = function(/*Node*/handler) {
this._inputNodes.push(handler);
}
WebSocketListenerNode.prototype.removeInputNode = function(/*Node*/handler) {
this._inputNodes.forEach(function(node, i, inputNodes) {
if (node === handler) {
inputNodes.splice(i, 1);
}
});
}
WebSocketListenerNode.prototype.handleEvent = function(id,/*socket*/socket,/*String*/event,/*Object*/data,/*Object*/flags) {
var msg;
if (this.wholemsg) {
try {
msg = JSON.parse(data);
if (typeof msg !== "object" && !Array.isArray(msg) && (msg !== null)) {
msg = { payload:msg };
}
}
catch(err) {
msg = { payload:data };
}
} else {
msg = {
payload:data
};
}
msg._session = {type:"websocket",id:id};
for (var i = 0; i < this._inputNodes.length; i++) {
this._inputNodes[i].send(msg);
}
}
WebSocketListenerNode.prototype.broadcast = function(data) {
if (this.isServer) {
for (let client in this._clients) {
if (this._clients.hasOwnProperty(client)) {
try {
this._clients[client].send(data);
} catch(err) {
this.warn(RED._("websocket.errors.send-error")+" "+client+" "+err.toString())
}
}
}
}
else {
try {
this.server.send(data);
} catch(err) {
this.warn(RED._("websocket.errors.send-error")+" "+err.toString())
}
}
}
WebSocketListenerNode.prototype.reply = function(id,data) {
var session = this._clients[id];
if (session) {
try {
session.send(data);
}
catch(e) { // swallow any errors
}
}
}
function WebSocketInNode(n) {
RED.nodes.createNode(this,n);
this.server = (n.client)?n.client:n.server;
var node = this;
this.serverConfig = RED.nodes.getNode(this.server);
if (this.serverConfig) {
this.serverConfig.registerInputNode(this);
// TODO: nls
this.serverConfig.on('opened', function(event) {
node.status({
fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count}),
event:"connect",
_session: {type:"websocket",id:event.id}
});
});
this.serverConfig.on('erro', function(event) {
node.status({
fill:"red",shape:"ring",text:"common.status.error",
event:"error",
_session: {type:"websocket",id:event.id}
});
});
this.serverConfig.on('closed', function(event) {
var status;
if (event.count > 0) {
status = {fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count})};
} else {
status = {fill:"red",shape:"ring",text:"common.status.disconnected"};
}
status.event = "disconnect";
status._session = {type:"websocket",id:event.id}
node.status(status);
});
} else {
this.error(RED._("websocket.errors.missing-conf"));
}
this.on('close', function() {
if (node.serverConfig) {
node.serverConfig.removeInputNode(node);
}
node.status({});
});
}
RED.nodes.registerType("websocket in",WebSocketInNode);
function WebSocketOutNode(n) {
RED.nodes.createNode(this,n);
var node = this;
this.server = (n.client)?n.client:n.server;
this.serverConfig = RED.nodes.getNode(this.server);
if (!this.serverConfig) {
return this.error(RED._("websocket.errors.missing-conf"));
}
else {
// TODO: nls
this.serverConfig.on('opened', function(event) {
node.status({
fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count}),
event:"connect",
_session: {type:"websocket",id:event.id}
});
});
this.serverConfig.on('erro', function(event) {
node.status({
fill:"red",shape:"ring",text:"common.status.error",
event:"error",
_session: {type:"websocket",id:event.id}
})
});
this.serverConfig.on('closed', function(event) {
var status;
if (event.count > 0) {
status = {fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count})};
} else {
status = {fill:"red",shape:"ring",text:"common.status.disconnected"};
}
status.event = "disconnect";
status._session = {type:"websocket",id:event.id}
node.status(status);
});
}
this.on("input", function(msg, nodeSend, nodeDone) {
var payload;
if (this.serverConfig.wholemsg) {
var sess;
if (msg._session) { sess = JSON.stringify(msg._session); }
delete msg._session;
payload = JSON.stringify(msg);
if (sess) { msg._session = JSON.parse(sess); }
}
else if (msg.hasOwnProperty("payload")) {
if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string.
payload = RED.util.ensureString(msg.payload);
}
else {
payload = msg.payload;
}
}
if (payload) {
if (msg._session && msg._session.type == "websocket") {
node.serverConfig.reply(msg._session.id,payload);
} else {
node.serverConfig.broadcast(payload,function(error) {
if (!!error) {
node.warn(RED._("websocket.errors.send-error")+inspect(error));
}
});
}
}
nodeDone();
});
this.on('close', function() {
node.status({});
});
}
RED.nodes.registerType("websocket out",WebSocketOutNode);
}

View File

@ -1,383 +0,0 @@
<!--
Copyright JS Foundation and other contributors, http://js.foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/html" data-template-name="tcp in">
<div class="form-row">
<label for="node-input-server"><i class="fa fa-dot-circle-o"></i> <span data-i18n="tcpin.label.type"></span></label>
<select id="node-input-server" style="width:120px; margin-right:5px;">
<option value="server" data-i18n="tcpin.type.listen"></option>
<option value="client" data-i18n="tcpin.type.connect"></option>
</select>
<span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width:65px">
</div>
<div class="form-row hidden" id="node-input-host-row" style="padding-left:110px;">
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width:auto; vertical-align:top;">
<label for="node-input-usetls" style="width:auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width:auto; margin-left:20px; margin-right:10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row">
<label><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.output"></span></label>
<select id="node-input-datamode" style="width:110px;">
<option value="stream" data-i18n="tcpin.output.stream"></option>
<option value="single" data-i18n="tcpin.output.single"></option>
</select>
<select id="node-input-datatype" style="width:140px;">
<option value="buffer" data-i18n="tcpin.output.buffer"></option>
<option value="utf8" data-i18n="tcpin.output.string"></option>
<option value="base64" data-i18n="tcpin.output.base64"></option>
</select>
<span data-i18n="tcpin.label.payload"></span>
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="common.label.topic"></span></label>
<input type="text" id="node-input-topic" data-i18n="[placeholder]common.label.topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('tcp in',{
category: 'network',
color: "Silver",
defaults: {
name: {value:""},
server: {value:"server", required:true},
host: {value:"", validate:function(v) { return (this.server == "server")||v.length > 0;} },
port: {value:"", required:true, validate:RED.validators.number()},
datamode:{value:"stream"},
datatype:{value:"buffer"},
newline:{value:""},
topic: {value:""},
base64: {/*deprecated*/ value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
},
inputs:0,
outputs:1,
icon: "bridge-dash.svg",
label: function() {
return this.name || "tcp:"+(this.host?this.host+":":"")+this.port;
},
labelStyle: function() {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
var updateOptions = function() {
var sockettype = $("#node-input-server").val();
if (sockettype == "client") {
$("#node-input-host-row").show();
} else {
$("#node-input-host-row").hide();
}
var datamode = $("#node-input-datamode").val();
var datatype = $("#node-input-datatype").val();
if (datamode == "stream") {
if (datatype == "utf8") {
$("#node-row-newline").show();
} else {
$("#node-row-newline").hide();
}
} else {
$("#node-row-newline").hide();
}
};
updateOptions();
$("#node-input-server").change(updateOptions);
$("#node-input-datatype").change(updateOptions);
$("#node-input-datamode").change(updateOptions);
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>
<script type="text/html" data-template-name="tcp out">
<div class="form-row">
<label for="node-input-beserver"><i class="fa fa-dot-circle-o"></i> <span data-i18n="tcpin.label.type"></span></label>
<select id="node-input-beserver" style="width:150px; margin-right:5px;">
<option value="server" data-i18n="tcpin.type.listen"></option>
<option value="client" data-i18n="tcpin.type.connect"></option>
<option value="reply" data-i18n="tcpin.type.reply"></option>
</select>
<span id="node-input-port-row"><span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width: 65px"></span>
</div>
<div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;">
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" style="width: 60%;">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row hidden" id="node-input-end-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-end" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-end" style="width: 70%;"><span data-i18n="tcpin.label.close-connection"></span></label>
</div>
<div class="form-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-base64" placeholder="base64" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-base64" style="width: 70%;"><span data-i18n="tcpin.label.decode-base64"></span></label>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('tcp out',{
category: 'network',
color: "Silver",
defaults: {
name: {value:""},
host: {value:"",validate:function(v) { return (this.beserver != "client")||v.length > 0;} },
port: {value:"",validate:function(v) { return (this.beserver == "reply")||RED.validators.number()(v); } },
beserver: {value:"client", required:true},
base64: {value:false, required:true},
end: {value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
},
inputs:1,
outputs:0,
icon: "bridge-dash.svg",
align: "right",
label: function() {
return this.name || "tcp:"+(this.host?this.host+":":"")+this.port;
},
labelStyle: function() {
return (this.name)?"node_label_italic":"";
},
oneditprepare: function() {
var updateOptions = function() {
var sockettype = $("#node-input-beserver").val();
if (sockettype == "reply") {
$("#node-input-port-row").hide();
$("#node-input-host-row").hide();
$("#node-input-end-row").hide();
$("#node-input-tls-enable").hide();
} else if (sockettype == "client"){
$("#node-input-port-row").show();
$("#node-input-host-row").show();
$("#node-input-end-row").show();
$("#node-input-tls-enable").show();
} else {
$("#node-input-port-row").show();
$("#node-input-host-row").hide();
$("#node-input-end-row").show();
$("#node-input-tls-enable").show();
}
};
updateOptions();
$("#node-input-beserver").change(updateOptions);
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>
<script type="text/html" data-template-name="tcp request">
<div class="form-row">
<label for="node-input-server"><i class="fa fa-globe"></i> <span data-i18n="tcpin.label.server"></span></label>
<input type="text" id="node-input-server" placeholder="ip.address" style="width:45%">
<span data-i18n="tcpin.label.port"></span>
<input type="text" id="node-input-port" style="width:60px">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row">
<label for="node-input-ret"><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.return"></span></label>
<select type="text" id="node-input-ret" style="width:54%;">
<option value="buffer" data-i18n="tcpin.output.buffer"></option>
<option value="string" data-i18n="tcpin.output.string"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-out"><i class="fa fa-sign-out fa-rotate-90"></i> <span data-i18n="tcpin.label.close"></span></label>
<select type="text" id="node-input-out" style="width:54%;">
<option value="time" data-i18n="tcpin.return.timeout"></option>
<option value="char" data-i18n="tcpin.return.character"></option>
<option value="count" data-i18n="tcpin.return.number"></option>
<option value="sit" data-i18n="tcpin.return.never"></option>
<option value="immed" data-i18n="tcpin.return.immed"></option>
</select>
<input type="text" id="node-input-splitc" style="width:50px;">
<span id="node-units"></span>
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:162px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('tcp request',{
category: 'network',
color: "Silver",
defaults: {
name: {value:""},
server: {value:""},
port: {value:"", validate:RED.validators.regex(/^(\d*|)$/)},
out: {value:"time", required:true},
ret: {value:"buffer"},
splitc: {value:"0", required:true},
newline: {value:""},
tls: {type:"tls-config", value:'', required:false}
},
inputs:1,
outputs:1,
icon: "bridge-dash.svg",
label: function() {
return this.name || "tcp:"+(this.server?this.server+":":"")+this.port;
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var previous = null;
if ($("#node-input-ret").val() == undefined) {
$("#node-input-ret").val("buffer");
this.ret = "buffer";
}
$("#node-input-ret").on("change", function() {
if ($("#node-input-ret").val() === "string" && $("#node-input-out").val() === "sit") { $("#node-row-newline").show(); }
else { $("#node-row-newline").hide(); }
});
$("#node-input-out").on("change", function() {
if ($("#node-input-ret").val() === "string" && $("#node-input-out").val() === "sit") { $("#node-row-newline").show(); }
else { $("#node-row-newline").hide(); }
});
$("#node-input-out").on('focus', function () { previous = this.value; }).on("change", function() {
$("#node-input-splitc").show();
if (previous === null) { previous = $("#node-input-out").val(); }
if ($("#node-input-out").val() == "char") {
if (previous != "char") { $("#node-input-splitc").val("\\n"); }
$("#node-units").text("");
}
else if ($("#node-input-out").val() == "time") {
if (previous != "time") { $("#node-input-splitc").val("0"); }
$("#node-units").text(RED._("node-red:tcpin.label.ms"));
}
else if ($("#node-input-out").val() == "immed") {
if (previous != "immed") { $("#node-input-splitc").val(" "); }
$("#node-units").text("");
$("#node-input-splitc").hide();
}
else if ($("#node-input-out").val() == "count") {
if (previous != "count") { $("#node-input-splitc").val("12"); }
$("#node-units").text(RED._("node-red:tcpin.label.chars"));
}
else {
if (previous != "sit") { $("#node-input-splitc").val(" "); }
$("#node-units").text("");
$("#node-input-splitc").hide();
}
});
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>

View File

@ -1,852 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
let reconnectTime = RED.settings.socketReconnectTime || 10000;
let socketTimeout = RED.settings.socketTimeout || null;
const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000;
const Denque = require('denque');
const net = require('net');
const tls = require('tls');
let connectionPool = {};
function normalizeConnectArgs(listArgs) {
const args = net._normalizeArgs(listArgs);
const options = args[0];
const cb = args[1];
// If args[0] was options, then normalize dealt with it.
// If args[0] is port, or args[0], args[1] is host, port, we need to
// find the options and merge them in, normalize's options has only
// the host/port/path args that it knows about, not the tls options.
// This means that options.host overrides a host arg.
if (listArgs[1] !== null && typeof listArgs[1] === 'object') {
ObjectAssign(options, listArgs[1]);
} else if (listArgs[2] !== null && typeof listArgs[2] === 'object') {
ObjectAssign(options, listArgs[2]);
}
return cb ? [options, cb] : [options];
}
function getAllowUnauthorized() {
const allowUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
if (allowUnauthorized) {
process.emitWarning(
'Setting the NODE_TLS_REJECT_UNAUTHORIZED ' +
'environment variable to \'0\' makes TLS connections ' +
'and HTTPS requests insecure by disabling ' +
'certificate verification.');
}
return allowUnauthorized;
}
/**
* Enqueue `item` in `queue`
* @param {Denque} queue - Queue
* @param {*} item - Item to enqueue
* @private
* @returns {Denque} `queue`
*/
const enqueue = (queue, item) => {
// drop msgs from front of queue if size is going to be exceeded
if (queue.length === msgQueueSize) { queue.shift(); }
queue.push(item);
return queue;
};
/**
* Shifts item off front of queue
* @param {Deque} queue - Queue
* @private
* @returns {*} Item previously at front of queue
*/
const dequeue = queue => queue.shift();
function TcpIn(n) {
RED.nodes.createNode(this,n);
this.host = n.host;
this.port = n.port * 1;
this.topic = n.topic;
this.stream = (!n.datamode||n.datamode=='stream'); /* stream,single*/
this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.base64 = n.base64;
this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server");
this.closing = false;
this.connected = false;
var node = this;
var count = 0;
if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); }
if (!node.server) {
var buffer = null;
var client;
var reconnectTimeout;
var end = false;
var setupTcpClient = function() {
node.log(RED._("tcpin.status.connecting",{host:node.host,port:node.port}));
node.status({fill:"grey",shape:"dot",text:"common.status.connecting"});
var id = RED.util.generateId();
var connOpts = {host: node.host};
if (n.tls) {
var connOpts = tlsNode.addTLSOptions({host: node.host});
client = tls.connect(node.port, connOpts, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("status.connected", {host: node.host, port: node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
}
else {
client = net.connect(node.port, node.host, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
}
client.setKeepAlive(true, 120000);
connectionPool[id] = client;
client.on('data', function (data) {
if (node.datatype != 'buffer') {
data = data.toString(node.datatype);
}
if (node.stream) {
var msg;
if ((node.datatype) === "utf8" && node.newline !== "") {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd()};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
buffer = parts[parts.length-1];
} else {
msg = {topic:node.topic, payload:data};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
} else {
if ((typeof data) === "string") {
buffer = buffer+data;
} else {
buffer = Buffer.concat([buffer,data],buffer.length+data.length);
}
}
});
client.on('end', function() {
if (!node.stream || (node.datatype == "utf8" && node.newline !== "" && buffer.length > 0)) {
var msg = {topic:node.topic, payload:buffer};
msg._session = {type:"tcp",id:id};
if (buffer.length !== 0) {
end = true; // only ask for fast re-connect if we actually got something
node.send(msg);
}
buffer = null;
}
});
client.on('close', function() {
delete connectionPool[id];
node.connected = false;
node.status({fill:"red",shape:"ring",text:"common.status.disconnected",_session:{type:"tcp",id:id}});
if (!node.closing) {
if (end) { // if we were asked to close then try to reconnect once very quick.
end = false;
reconnectTimeout = setTimeout(setupTcpClient, 20);
}
else {
node.log(RED._("tcpin.errors.connection-lost",{host:node.host,port:node.port}));
reconnectTimeout = setTimeout(setupTcpClient, reconnectTime);
}
} else {
if (node.doneClose) { node.doneClose(); }
}
});
client.on('error', function(err) {
node.log(err);
});
}
setupTcpClient();
this.on('close', function(done) {
node.doneClose = done;
this.closing = true;
if (client) { client.destroy(); }
clearTimeout(reconnectTimeout);
if (!node.connected) { done(); }
});
}
else {
let srv = net;
let connOpts;
if (n.tls) {
srv = tls;
connOpts = tlsNode.addTLSOptions({});
}
var server = srv.createServer(connOpts, function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
var id = RED.util.generateId();
var fromi;
var fromp;
connectionPool[id] = socket;
count++;
node.status({
text:RED._("tcpin.status.connections",{count:count}),
event:"connect",
ip:socket.remoteAddress,
port:socket.remotePort,
_session: {type:"tcp",id:id}
});
var buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
socket.on('data', function (data) {
if (node.datatype != 'buffer') {
data = data.toString(node.datatype);
}
if (node.stream) {
var msg;
if ((typeof data) === "string" && node.newline !== "") {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd(), ip:socket.remoteAddress, port:socket.remotePort};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
buffer = parts[parts.length-1];
} else {
msg = {topic:node.topic, payload:data, ip:socket.remoteAddress, port:socket.remotePort};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
}
else {
if ((typeof data) === "string") {
buffer = buffer+data;
} else {
buffer = Buffer.concat([buffer,data],buffer.length+data.length);
}
fromi = socket.remoteAddress;
fromp = socket.remotePort;
}
});
socket.on('end', function() {
if (!node.stream || (node.datatype === "utf8" && node.newline !== "") || (node.datatype === "base64")) {
if (buffer.length > 0) {
var msg = {topic:node.topic, payload:buffer, ip:fromi, port:fromp};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
buffer = null;
}
});
socket.on('timeout', function() {
node.log(RED._("tcpin.errors.timeout",{port:node.port}));
socket.end();
});
socket.on('close', function() {
delete connectionPool[id];
count--;
node.status({
text:RED._("tcpin.status.connections",{count:count}),
event:"disconnect",
ip:socket.remoteAddress,
port:socket.remotePort,
_session: {type:"tcp",id:id}
});
});
socket.on('error',function(err) {
node.log(err);
});
});
server.on('error', function(err) {
if (err) {
node.error(RED._("tcpin.errors.cannot-listen",{port:node.port,error:err.toString()}));
}
});
server.listen(node.port, function(err) {
if (err) {
node.error(RED._("tcpin.errors.cannot-listen",{port:node.port,error:err.toString()}));
} else {
node.log(RED._("tcpin.status.listening-port",{port:node.port}));
node.on('close', function() {
for (var c in connectionPool) {
if (connectionPool.hasOwnProperty(c)) {
connectionPool[c].end();
connectionPool[c].unref();
}
}
node.closing = true;
server.close();
node.log(RED._("tcpin.status.stopped-listening",{port:node.port}));
});
}
});
}
}
RED.nodes.registerType("tcp in",TcpIn);
function TcpOut(n) {
RED.nodes.createNode(this,n);
this.host = n.host;
this.port = n.port * 1;
this.base64 = n.base64;
this.doend = n.end || false;
this.beserver = n.beserver;
this.name = n.name;
this.closing = false;
this.connected = false;
var node = this;
if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); }
if (!node.beserver || node.beserver == "client") {
var reconnectTimeout;
var client = null;
var end = false;
var setupTcpClient = function() {
node.log(RED._("tcpin.status.connecting",{host:node.host,port:node.port}));
node.status({fill:"grey",shape:"dot",text:"common.status.connecting"});
if (n.tls) {
// connOpts = tlsNode.addTLSOptions(connOpts);
// client = tls.connect(connOpts, function() {
var connOpts = tlsNode.addTLSOptions({host: node.host});
client = tls.connect(node.port, connOpts, function() {
// buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("status.connected", {host: node.host, port: node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
else {
client = net.connect(node.port, node.host, function() {
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
client.setKeepAlive(true,120000);
client.on('error', function (err) {
node.log(RED._("tcpin.errors.error",{error:err.toString()}));
});
client.on('end', function (err) {
node.status({});
node.connected = false;
});
client.on('close', function() {
node.status({fill:"red",shape:"ring",text:"common.status.disconnected"});
node.connected = false;
client.destroy();
if (!node.closing) {
if (end) {
end = false;
reconnectTimeout = setTimeout(setupTcpClient,20);
}
else {
node.log(RED._("tcpin.errors.connection-lost",{host:node.host,port:node.port}));
reconnectTimeout = setTimeout(setupTcpClient,reconnectTime);
}
} else {
if (node.doneClose) { node.doneClose(); }
}
});
}
setupTcpClient();
node.on("input", function(msg, nodeSend, nodeDone) {
if (node.connected && msg.payload != null) {
if (Buffer.isBuffer(msg.payload)) {
client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
}
if (node.doend === true) {
end = true;
if (client) { node.status({}); client.destroy(); }
}
}
nodeDone();
});
node.on("close", function(done) {
node.doneClose = done;
this.closing = true;
if (client) { client.destroy(); }
clearTimeout(reconnectTimeout);
if (!node.connected) { done(); }
});
}
else if (node.beserver == "reply") {
node.on("input",function(msg, nodeSend, nodeDone) {
if (msg._session && msg._session.type == "tcp") {
var client = connectionPool[msg._session.id];
if (client) {
if (Buffer.isBuffer(msg.payload)) {
client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
}
}
}
else {
for (var i in connectionPool) {
if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
} else {
connectionPool[i].write(Buffer.from(""+msg.payload));
}
}
}
nodeDone();
});
}
else {
var connectedSockets = [];
node.status({text:RED._("tcpin.status.connections",{count:0})});
let srv = net;
let connOpts;
if (n.tls) {
srv = tls;
connOpts = tlsNode.addTLSOptions({});
}
var server = srv.createServer(connOpts, function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
node.log(RED._("tcpin.status.connection-from",{host:socket.remoteAddress, port:socket.remotePort}));
socket.on('timeout', function() {
node.log(RED._("tcpin.errors.timeout",{port:node.port}));
socket.end();
});
socket.on('data', function(d) {
// console.log("DATA",d)
});
socket.on('close',function() {
node.log(RED._("tcpin.status.connection-closed",{host:socket.remoteAddress, port:socket.remotePort}));
connectedSockets.splice(connectedSockets.indexOf(socket),1);
node.status({text:RED._("tcpin.status.connections",{count:connectedSockets.length})});
});
socket.on('error',function() {
node.log(RED._("tcpin.errors.socket-error",{host:socket.remoteAddress, port:socket.remotePort}));
connectedSockets.splice(connectedSockets.indexOf(socket),1);
node.status({text:RED._("tcpin.status.connections",{count:connectedSockets.length})});
});
connectedSockets.push(socket);
node.status({text:RED._("tcpin.status.connections",{count:connectedSockets.length})});
});
node.on("input", function(msg, nodeSend, nodeDone) {
if (msg.payload != null) {
var buffer;
if (Buffer.isBuffer(msg.payload)) {
buffer = msg.payload;
} else if (typeof msg.payload === "string" && node.base64) {
buffer = Buffer.from(msg.payload,'base64');
} else {
buffer = Buffer.from(""+msg.payload);
}
for (var i = 0; i < connectedSockets.length; i += 1) {
if (node.doend === true) { connectedSockets[i].end(buffer); }
else { connectedSockets[i].write(buffer); }
}
}
nodeDone();
});
server.on('error', function(err) {
if (err) {
node.error(RED._("tcpin.errors.cannot-listen",{port:node.port,error:err.toString()}));
}
});
server.listen(node.port, function(err) {
if (err) {
node.error(RED._("tcpin.errors.cannot-listen",{port:node.port,error:err.toString()}));
} else {
node.log(RED._("tcpin.status.listening-port",{port:node.port}));
node.on('close', function() {
for (var c in connectedSockets) {
if (connectedSockets.hasOwnProperty(c)) {
connectedSockets[c].end();
connectedSockets[c].unref();
}
}
server.close();
node.log(RED._("tcpin.status.stopped-listening",{port:node.port}));
});
}
});
}
}
RED.nodes.registerType("tcp out",TcpOut);
function TcpGet(n) {
RED.nodes.createNode(this,n);
this.server = n.server;
this.port = Number(n.port);
this.out = n.out;
this.ret = n.ret || "buffer";
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.splitc = n.splitc;
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
if (this.out === "immed") { this.splitc = -1; this.out = "time"; }
if (this.out !== "char") { this.splitc = Number(this.splitc); }
else {
if (this.splitc[0] == '\\') {
this.splitc = parseInt(this.splitc.replace("\\n",0x0A).replace("\\r",0x0D).replace("\\t",0x09).replace("\\e",0x1B).replace("\\f",0x0C).replace("\\0",0x00));
} // jshint ignore:line
if (typeof this.splitc == "string") {
if (this.splitc.substr(0,2) == "0x") {
this.splitc = parseInt(this.splitc);
}
else {
this.splitc = this.splitc.charCodeAt(0);
}
} // jshint ignore:line
}
var node = this;
var clients = {};
this.on("input", function(msg, nodeSend, nodeDone) {
var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
var host = node.server || msg.host;
var port = node.port || msg.port;
// Store client information independently
// the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout
var connection_id = host + ":" + port;
if (connection_id !== node.last_id) {
node.status({});
node.last_id = connection_id;
}
clients[connection_id] = clients[connection_id] || {
msgQueue: new Denque(),
connected: false,
connecting: false
};
enqueue(clients[connection_id].msgQueue, {msg:msg, nodeSend:nodeSend, nodeDone:nodeDone});
clients[connection_id].lastMsg = msg;
if (!clients[connection_id].connecting && !clients[connection_id].connected) {
var buf;
if (this.out == "count") {
if (this.splitc === 0) { buf = Buffer.alloc(1); }
else { buf = Buffer.alloc(this.splitc); }
}
else { buf = Buffer.alloc(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully
var connOpts = {host:host, port:port};
if (n.tls) {
connOpts = tlsNode.addTLSOptions(connOpts);
const allowUnauthorized = getAllowUnauthorized();
let options = {
rejectUnauthorized: !allowUnauthorized,
ciphers: tls.DEFAULT_CIPHERS,
checkServerIdentity: tls.checkServerIdentity,
minDHSize: 1024,
...connOpts
};
if (!options.keepAlive) { options.singleUse = true; }
const context = options.secureContext || tls.createSecureContext(options);
clients[connection_id].client = new tls.TLSSocket(options.socket, {
allowHalfOpen: options.allowHalfOpen,
pipe: !!options.path,
secureContext: context,
isServer: false,
requestCert: false, // true,
rejectUnauthorized: false, // options.rejectUnauthorized !== false,
session: options.session,
ALPNProtocols: options.ALPNProtocols,
requestOCSP: options.requestOCSP,
enableTrace: options.enableTrace,
pskCallback: options.pskCallback,
highWaterMark: options.highWaterMark,
onread: options.onread,
signal: options.signal,
});
}
else {
clients[connection_id].client = net.Socket();
}
if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);}
if (host && port) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(connOpts, function() {
//node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = true;
clients[connection_id].connecting = false;
let event;
while (event = dequeue(clients[connection_id].msgQueue)) {
clients[connection_id].client.write(event.msg.payload);
event.nodeDone();
}
if (node.out === "time" && node.splitc < 0) {
clients[connection_id].connected = clients[connection_id].connecting = false;
clients[connection_id].client.end();
delete clients[connection_id];
node.status({});
}
}
});
}
else {
node.warn(RED._("tcpin.errors.no-host"));
}
var chunk = "";
clients[connection_id].client.on('data', function(data) {
if (node.out === "sit") { // if we are staying connected just send the buffer
if (clients[connection_id]) {
const msg = clients[connection_id].lastMsg || {};
msg.payload = RED.util.cloneMessage(data);
if (node.ret === "string") {
try {
if (node.newline && node.newline !== "" ) {
chunk += msg.payload.toString();
let parts = chunk.split(node.newline);
for (var p=0; p<parts.length-1; p+=1) {
let m = RED.util.cloneMessage(msg);
m.payload = parts[p] + node.newline.trimEnd();
nodeSend(m);
}
chunk = parts[parts.length-1];
}
else {
msg.payload = msg.payload.toString();
nodeSend(msg);
}
}
catch(e) { node.error(RED._("tcpin.errors.bad-string"), msg); }
}
else { nodeSend(msg); }
}
}
// else if (node.splitc === 0) {
// clients[connection_id].msg.payload = data;
// node.send(clients[connection_id].msg);
// }
else {
for (var j = 0; j < data.length; j++ ) {
if (node.out === "time") {
if (clients[connection_id]) {
// do the timer thing
if (clients[connection_id].timeout) {
i += 1;
buf[i] = data[j];
}
else {
clients[connection_id].timeout = setTimeout(function () {
if (clients[connection_id]) {
clients[connection_id].timeout = null;
const msg = clients[connection_id].lastMsg || {};
msg.payload = Buffer.alloc(i+1);
buf.copy(msg.payload,0,0,i+1);
if (node.ret === "string") {
try { msg.payload = msg.payload.toString(); }
catch(e) { node.error("Failed to create string", msg); }
}
nodeSend(msg);
if (clients[connection_id].client) {
node.status({});
clients[connection_id].client.destroy();
delete clients[connection_id];
}
}
}, node.splitc);
i = 0;
buf[0] = data[j];
}
}
}
// count bytes into a buffer...
else if (node.out == "count") {
buf[i] = data[j];
i += 1;
if ( i >= node.splitc) {
if (clients[connection_id]) {
const msg = clients[connection_id].lastMsg || {};
msg.payload = Buffer.alloc(i);
buf.copy(msg.payload,0,0,i);
if (node.ret === "string") {
try { msg.payload = msg.payload.toString(); }
catch(e) { node.error("Failed to create string", msg); }
}
nodeSend(msg);
if (clients[connection_id].client) {
node.status({});
clients[connection_id].client.destroy();
delete clients[connection_id];
}
i = 0;
}
}
}
// look for a char
else {
buf[i] = data[j];
i += 1;
if (data[j] == node.splitc) {
if (clients[connection_id]) {
const msg = clients[connection_id].lastMsg || {};
msg.payload = Buffer.alloc(i);
buf.copy(msg.payload,0,0,i);
if (node.ret === "string") {
try { msg.payload = msg.payload.toString(); }
catch(e) { node.error("Failed to create string", msg); }
}
nodeSend(msg);
if (clients[connection_id].client) {
node.status({});
clients[connection_id].client.destroy();
delete clients[connection_id];
}
i = 0;
}
}
}
}
}
});
clients[connection_id].client.on('end', function() {
//console.log("END");
node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"});
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = clients[connection_id].connecting = false;
clients[connection_id].client = null;
}
});
clients[connection_id].client.on('close', function() {
//console.log("CLOSE");
if (clients[connection_id]) {
clients[connection_id].connected = clients[connection_id].connecting = false;
}
var anyConnected = false;
for (var client in clients) {
if (clients[client].connected) {
anyConnected = true;
break;
}
}
if (node.doneClose && !anyConnected) {
clients = {};
node.doneClose();
}
});
clients[connection_id].client.on('error', function() {
//console.log("ERROR");
node.status({fill:"red",shape:"ring",text:"common.status.error"});
node.error(RED._("tcpin.errors.connect-fail") + " " + connection_id, msg);
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].client.destroy();
delete clients[connection_id];
}
});
clients[connection_id].client.on('timeout',function() {
//console.log("TIMEOUT");
if (clients[connection_id]) {
clients[connection_id].connected = clients[connection_id].connecting = false;
node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"});
//node.warn(RED._("tcpin.errors.connect-timeout"));
if (clients[connection_id].client) {
clients[connection_id].connecting = true;
var connOpts = {host:host, port:port};
if (n.tls) {
connOpts = tlsNode.addTLSOptions(connOpts);
}
clients[connection_id].client.connect(connOpts, function() {
clients[connection_id].connected = true;
clients[connection_id].connecting = false;
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
}
});
}
else if (!clients[connection_id].connecting && clients[connection_id].connected) {
if (clients[connection_id] && clients[connection_id].client) {
let event = dequeue(clients[connection_id].msgQueue)
clients[connection_id].client.write(event.msg.payload);
event.nodeDone();
}
}
});
this.on("close", function(done) {
node.doneClose = done;
for (var cl in clients) {
if (clients[cl].hasOwnProperty("client")) {
clients[cl].client.destroy();
}
}
node.status({});
// this is probably not necessary and may be removed
var anyConnected = false;
for (var c in clients) {
if (clients[c].connected) {
anyConnected = true;
break;
}
}
if (!anyConnected) { clients = {}; }
done();
});
}
RED.nodes.registerType("tcp request",TcpGet);
}

View File

@ -1,138 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
function JSONNode(n) {
RED.nodes.createNode(this,n);
this.indent = n.pretty ? 4 : 0;
this.action = n.action||"";
this.property = n.property||"payload";
this.schema = null;
this.compiledSchema = null;
var node = this;
this.on("input", function(msg,send,done) {
var validate = false;
if (msg.schema) {
// If input schema is different, re-compile it
if (JSON.stringify(this.schema) != JSON.stringify(msg.schema)) {
try {
this.compiledSchema = ajv.compile(msg.schema);
this.schema = msg.schema;
} catch(e) {
this.schema = null;
this.compiledSchema = null;
done(RED._("json.errors.schema-error-compile"));
return;
}
}
validate = true;
}
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (typeof value === "string" || Buffer.isBuffer(value)) {
// if (Buffer.isBuffer(value) && node.action !== "obj") {
// node.warn(RED._("json.errors.dropped")); done();
// }
// else
if (node.action === "" || node.action === "obj") {
try {
RED.util.setMessageProperty(msg,node.property,JSON.parse(value));
if (validate) {
if (this.compiledSchema(msg[node.property])) {
delete msg.schema;
send(msg);
done();
} else {
msg.schemaError = this.compiledSchema.errors;
done(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`);
}
} else {
send(msg);
done();
}
}
catch(e) { done(e.message); }
} else {
// If node.action is str and value is str
if (validate) {
if (this.compiledSchema(JSON.parse(msg[node.property]))) {
delete msg.schema;
send(msg);
done();
} else {
msg.schemaError = this.compiledSchema.errors;
done(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`);
}
} else {
send(msg);
done();
}
}
}
else if ((typeof value === "object") || (typeof value === "boolean") || (typeof value === "number")) {
if (node.action === "" || node.action === "str") {
if (!Buffer.isBuffer(value)) {
try {
if (validate) {
if (this.compiledSchema(value)) {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
delete msg.schema;
send(msg);
done();
} else {
msg.schemaError = this.compiledSchema.errors;
done(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`);
}
} else {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
send(msg);
done();
}
}
catch(e) { done(RED._("json.errors.dropped-error")); }
}
else { node.warn(RED._("json.errors.dropped-object")); done(); }
} else {
// If node.action is obj and value is object
if (validate) {
if (this.compiledSchema(value)) {
delete msg.schema;
send(msg);
done();
} else {
msg.schemaError = this.compiledSchema.errors;
done(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`);
}
} else {
send(msg);
done();
}
}
}
else { node.warn(RED._("json.errors.dropped")); done(); }
}
else { send(msg); done(); } // If no property - just pass it on.
});
}
RED.nodes.registerType("json",JSONNode);
}

View File

@ -0,0 +1 @@
<svg width="512px" height="512px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M102.5 26.03l90.03 345.75 289.22 23.25-90.063-345.75L102.5 26.03zm-18.906 1.564c-30.466 11.873-55.68 53.098-49.75 75.312l3.25 11.78c.667-1.76 1.36-3.522 2.093-5.28C49.097 85.7 65.748 62.64 89.564 50.5l-5.97-22.906zm10.844 41.593c-16.657 10.012-29.92 28.077-38 47.407-5.247 12.55-8.038 25.63-8.75 36.53L112.5 388.407c.294-.55.572-1.106.875-1.656 10.603-19.252 27.823-37.695 51.125-48.47L94.437 69.19zm74.874 287.594c-17.677 9.078-31.145 23.717-39.562 39-4.464 8.107-7.27 16.364-8.688 23.75l11.688 42.408 1.625.125c-3.84-27.548 11.352-60.504 41.25-81.094l-6.313-24.19zm26.344 34c-32.567 17.27-46.51 52.44-41.844 72.94l289.844 24.5c-5.34-7.79-8.673-17.947-8.594-28.5l-22.406-9L459 443.436l-13.5-12.875c5.604-6.917 13.707-13.05 24.813-17.687L195.656 390.78z"/></svg>

After

Width:  |  Height:  |  Size: 874 B