Browse Source

2020-06-06 08:57

zry 3 years ago
parent
commit
050960cc14
3 changed files with 393 additions and 0 deletions
  1. 371 0
      TelwsView.vue
  2. 9 0
      index.js
  3. 13 0
      package.json

+ 371 - 0
TelwsView.vue

@@ -0,0 +1,371 @@
+<template>
+  <v-layout
+    column
+    fill-height
+  >
+    <v-toolbar color="grey">
+      <v-toolbar-title flex>
+        {{ title }}
+      </v-toolbar-title>
+      <v-spacer />
+      <v-text-field
+        v-if="this.hideUrl == undefined"
+        label="Telws URL"
+        single-line
+        prepend-icon="mdi-link-variant"
+        hide-details
+        v-model="telwsurl"
+      />
+      <v-btn
+        icon
+        @click="connect"
+      >
+        <v-icon>mdi-lan-connect</v-icon>
+      </v-btn>
+      <v-btn
+        icon
+        @click="disconnect"
+      >
+        <v-icon>mdi-lan-disconnect</v-icon>
+      </v-btn>
+    </v-toolbar>
+    <v-layout fill-height>
+      <xterm-vue
+        ref="term"
+        v-on:title-change="titleChange"
+      />
+    </v-layout>
+    <v-toolbar color="grey">
+      <v-text-field
+        placeholder="Write here and press Enter to send..."
+        single-line
+        prepend-icon="mdi-typewriter"
+        hide-details
+        v-model="writeBarText"
+        @keydown.enter="pasteWriteBar"
+      />
+    </v-toolbar>
+    <v-dialog
+      v-model="userAuthDialog"
+      max-width="290"
+    >
+      <v-card>
+        <v-card-title class="headline">Simple AAA Auth</v-card-title>
+
+        <v-card-text>
+          <v-form>
+            <v-text-field
+              ref="authUsername"
+              v-model="authDialogUsername"
+              label="Username"
+              @keydown.enter="focusPasswordTextfield"
+            />
+            <v-text-field
+              ref="authPassword"
+              v-model="authDialogPassword"
+              label="Password"
+              type="password"
+              @keydown.enter="doUserLogin"
+            />
+          </v-form>
+        </v-card-text>
+
+        <v-card-actions>
+          <v-spacer></v-spacer>
+
+          <v-btn
+            color="danger"
+            text
+            @click="cancelUserLogin"
+          >
+            Cancel
+          </v-btn>
+
+          <v-btn
+            color="success"
+            text
+            @click="doUserLogin"
+          >
+            Login
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </v-layout>
+</template>
+
+<script>
+//import XtermVuePlugin from "@swzry/xterm-vue";
+import axios from "axios";
+import smcrypto from "sm-crypto";
+const ConnState = {
+  IDLE: 0,
+  GET_INFO: 1,
+  USER_AUTH: 2,
+  SERVER_AUTH: 3,
+  AUTH_OK: 4,
+  CONNECTED: 5
+};
+export default {
+  name: "telws_view",
+  props: ["hideUrl", "defaultUrl", "autoConnect"],
+  data: () => ({
+    writeBarText: "",
+    title: "Untitled",
+    telwsurl: "",
+    userAuthDialog: false,
+    authDialogUsername: "",
+    authDialogPassword: "",
+    connSM: {
+      state: ConnState.IDLE,
+      axiosCancel: undefined,
+      continueDo: false,
+      url_prefix: "",
+      auth_jwt: "",
+      wsurl: "",
+      wsconn: undefined
+    },
+    encryptConfig: undefined
+  }),
+  mounted() {
+    this.$refs.term.fit();
+    if (this.defaultUrl) {
+      this.telwsurl = this.defaultUrl;
+    }
+    if (this.autoConnect != undefined) {
+      this.connect();
+    }
+  },
+  destroyed() {
+    this.disconnect();
+  },
+  methods: {
+    focusUsernameTextfield(){
+      this.$refs.authUsername.focus()
+    },
+    focusPasswordTextfield(){
+      this.$refs.authPassword.focus()
+    },
+    pasteWriteBar(event) {
+      this.$refs.term.paste(this.writeBarText);
+      this.writeBarText = "";
+    },
+    titleChange(title) {
+      this.title = title;
+    },
+    popUserLoginDialog() {
+      this.authDialogUsername = "";
+      this.authDialogPassword = "";
+      this.userAuthDialog = true;
+      var self = this
+      this.$nextTick(() => {
+        self.focusUsernameTextfield();
+      })
+    },
+    doUserLogin() {
+      this.userAuthDialog = false;
+      var authData = {
+        method: "simple-aaa",
+        username: this.authDialogUsername,
+        password: this.authDialogPassword
+      };
+      var jsondata = JSON.stringify(authData);
+      var cm = 1;
+      if (this.encryptConfig.cipherMode === "C1C3C2") {
+        cm = 1;
+      } else {
+        cm = 0;
+      }
+      if (this.encryptConfig) {
+        var ct = smcrypto.sm2.doEncrypt(
+          jsondata,
+          this.encryptConfig.pubkey,
+          cm
+        );
+        var tureCipher = this.encryptConfig.header + ct;
+        this.doLoginAuth(tureCipher);
+      } else {
+        this.connSM.state = ConnState.IDLE;
+        this.$refs.term.$term.writeln("Password encypt failed.");
+        this.$refs.term.$term.onData(function(data) {});
+      }
+    },
+    doWebsocketConnect() {
+      if (this.connSM.wsconn) {
+        this.connSM.wsconn.close();
+      }
+      this.$refs.term.$term.writeln(
+        "Connecting Websocket '" + this.connSM.wsurl + "'..."
+      );
+      var trueUrl = this.connSM.wsurl + "?token=" + this.connSM.auth_jwt
+      this.connSM.wsconn = new WebSocket(trueUrl);
+      var tty = this.$refs.term.$term;
+      var wsconn = this.connSM.wsconn;
+      var self = this
+      wsconn.onopen = function() {
+        //wsconn.binaryType = "arrayBuffer"
+        wsconn.binaryType = "blob"
+        tty.writeln("Connected.\r\n");
+        self.connSM.state = ConnState.CONNECTED
+      };
+      wsconn.onmessage = function(event) {
+        var er = new FileReader();
+        er.onload = function(e) {
+          var buf = new Uint8Array(er.result);
+          tty.write(buf);
+        };
+        er.readAsArrayBuffer(event.data);
+      };
+      wsconn.onclose = function() {
+        tty.writeln("\r\n\r\nConnection Closed.");
+      };
+      wsconn.onerror = function() {
+        tty.writeln("\r\n\r\nConnection Error.");
+      };
+      this.$refs.term.$term.onData(function(data) {
+        if (wsconn) {
+          var bd = Buffer.from(data, "utf8");
+          wsconn.send(bd);
+        }
+      });
+    },
+    doLoginAuth(authData) {
+      var adjson = { authData: authData };
+      var self = this;
+      this.connSM.state = ConnState.SERVER_AUTH;
+      axios
+        .post(this.connSM.url_prefix + "login.satori", adjson, {
+          cancelToken: new axios.CancelToken(function executor(c) {
+            self.connSM.cancelToken = c;
+          })
+        })
+        .then(resp => {
+          if (resp.data.suc) {
+            this.connSM.auth_jwt = resp.data.jwt;
+            this.connSM.state = ConnState.AUTH_OK;
+            this.doWebsocketConnect();
+          } else {
+            if (resp.data.etype == "auth_failed") {
+              this.$refs.term.$term.writeln("Simple AAA Auth Failed.");
+              this.$refs.term.$term.writeln(
+                "Reason: " + resp.data.auth_error_msg
+              );
+            } else {
+              this.$refs.term.$term.writeln("Failed to do remote auth.");
+              this.$refs.term.$term.writeln("Error: " + resp.data.etype);
+            }
+            this.connSM.state = ConnState.IDLE;
+          }
+        })
+        .catch(errmsg => {
+          this.$refs.term.$term.writeln("Failed to do remote auth.");
+          console.warn(errmsg);
+          if (errmsg.response) {
+            console.warn(errmsg.response);
+            this.$refs.term.$term.writeln("Error: " + errmsg.response.data);
+          } else if (errmsg.request) {
+            console.warn(errmsg.request);
+            this.$refs.term.$term.writeln(
+              "Error: request failed. See javascript console for detail."
+            );
+          } else {
+            this.$refs.term.$term.writeln(
+              "Error: unknown. See javascript console for more detail."
+            );
+          }
+          this.connSM.state = ConnState.IDLE;
+        });
+    },
+    cancelUserLogin() {
+      this.userAuthDialog = false;
+      this.connSM.state = ConnState.IDLE;
+      this.$refs.term.$term.writeln("Auth cancel by user.");
+      this.$refs.term.$term.onData(function(data) {});
+    },
+    disconnect() {
+      if (this.connSM.state == ConnState.IDLE) {
+        return;
+      }
+      this.connSM.continueDo = false;
+      if (this.connSM.cancelToken) {
+        this.connSM.cancelToken();
+      }
+      this.connSM.state = ConnState.IDLE;
+      this.$refs.term.$term.writeln("");
+      this.$refs.term.$term.writeln("Disconnected.");
+      this.$refs.term.$term.onData(function(data) {});
+      if(this.connSM.wsconn){
+        this.connSM.wsconn.close()
+      }
+    },
+    connect() {
+      if (this.connSM.state != ConnState.IDLE) {
+        this.disconnect();
+      }
+      this.$refs.term.$term.clear();
+      this.connSM.url_prefix = this.telwsurl + "/";
+      var turl = new URL(this.connSM.url_prefix);
+      var schpre = "ws:";
+      if (turl.protocol === "https:") {
+        schpre = "wss:";
+      }
+      this.connSM.wsurl = schpre + "//" + turl.host + turl.pathname + "ws.satori"
+      this.$refs.term.$term.writeln("Connecting " + this.telwsurl + "...");
+      var self = this;
+      this.connSM.continueDo = true;
+      this.connSM.state = ConnState.GET_INFO;
+      axios
+        .get(this.telwsurl, {
+          cancelToken: new axios.CancelToken(function executor(c) {
+            self.connSM.cancelToken = c;
+          })
+        })
+        .then(resp => {
+          if (self.connSM.continueDo) {
+            this.$refs.term.$term.writeln("Telws server info fetched.");
+            console.log(resp.data);
+            if (resp.data.auth_methods) {
+              if (resp.data.auth_methods.indexOf("simple-aaa") > -1) {
+                this.encryptConfig = resp.data.encrypt;
+                this.connSM.state = ConnState.USER_AUTH;
+                this.popUserLoginDialog();
+              } else {
+                this.$refs.term.$term.writeln(
+                  "Failed to connect to telws server."
+                );
+                this.$refs.term.$term.writeln(
+                  "Error: no auth method supported by this client."
+                );
+                this.connSM.state = ConnState.IDLE;
+              }
+            } else {
+              this.$refs.term.$term.writeln(
+                "Failed to connect to telws server."
+              );
+              this.$refs.term.$term.writeln("Error: not a valid telws server.");
+              this.connSM.state = ConnState.IDLE;
+            }
+          }
+        })
+        .catch(errmsg => {
+          this.$refs.term.$term.writeln("Failed to connect to telws server.");
+          console.warn(errmsg);
+          if (errmsg.response) {
+            console.warn(errmsg.response);
+            this.$refs.term.$term.writeln("Error: " + errmsg.response.data);
+          } else if (errmsg.request) {
+            console.warn(errmsg.request);
+            this.$refs.term.$term.writeln(
+              "Error: request failed. See javascript console for detail."
+            );
+          } else {
+            this.$refs.term.$term.writeln(
+              "Error: unknown. See javascript console for more detail."
+            );
+          }
+          this.connSM.state = ConnState.IDLE;
+        });
+    }
+  }
+};
+</script>

+ 9 - 0
index.js

@@ -0,0 +1,9 @@
+import TelwsView from './TelwsView.vue'
+import XtermVue from "@swzry/xterm-vue"
+
+export default {
+    install(Vue, options){
+        Vue.use(XtermVue)
+        Vue.component("telws-view", TelwsView)
+    }
+}

+ 13 - 0
package.json

@@ -0,0 +1,13 @@
+{
+  "name": "@swzry/satori-telws",
+  "version": "1.0.0",
+  "description": "A frontend package for a websocket based remote web terminal.",
+  "main": "index.js",
+  "repository": "https://git.swzry.com/zry/satori-telws.git",
+  "author": "zry <admin@z-touhou.org>",
+  "license": "MIT",
+  "private": false,
+  "dependencies": {
+    "@swzry/xterm-vue": "1.0.1"
+  }
+}