From 1796f13879cd9d00cbb7c0095295ee09bc723acd Mon Sep 17 00:00:00 2001 From: xinfengkun Date: Mon, 16 Jun 2025 16:04:05 +0800 Subject: [PATCH] =?UTF-8?q?[8.1.0][fmd]=20=E8=B0=83=E8=AF=95=E7=AA=97?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B0=83=E8=AF=95=E4=B8=AD=EF=BC=9A=E6=95=85?= =?UTF-8?q?=E9=9A=9C=E8=AF=8A=E6=96=AD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.gitignore | 1 + .../build.gradle | 92 + .../gradle.properties | 3 + .../proguard-rules.pro | 23 + .../src/main/AndroidManifest.xml | 29 + .../src/main/assets/AutoPilotVisualDB.db | Bin 0 -> 397312 bytes .../src/main/assets/AutoPilotVisualDB.db-shm | Bin 0 -> 32768 bytes .../src/main/assets/AutoPilotVisualDB.db-wal | Bin 0 -> 432632 bytes .../com/trilead/ssh2/AuthAgentCallback.java | 64 + .../com/trilead/ssh2/ChannelCondition.java | 61 + .../java/com/trilead/ssh2/Connection.java | 1564 +++++++++++++++ .../java/com/trilead/ssh2/ConnectionInfo.java | 65 + .../com/trilead/ssh2/ConnectionMonitor.java | 34 + .../com/trilead/ssh2/DHGexParameters.java | 121 ++ .../java/com/trilead/ssh2/DebugLogger.java | 23 + .../trilead/ssh2/DynamicPortForwarder.java | 77 + .../ssh2/ExtendedServerHostKeyVerifier.java | 47 + .../java/com/trilead/ssh2/ExtensionInfo.java | 47 + .../java/com/trilead/ssh2/HTTPProxyData.java | 202 ++ .../com/trilead/ssh2/HTTPProxyException.java | 29 + .../com/trilead/ssh2/InteractiveCallback.java | 55 + .../java/com/trilead/ssh2/KnownHosts.java | 894 +++++++++ .../com/trilead/ssh2/LocalPortForwarder.java | 61 + .../trilead/ssh2/LocalStreamForwarder.java | 74 + .../main/java/com/trilead/ssh2/ProxyData.java | 28 + .../main/java/com/trilead/ssh2/SCPClient.java | 747 +++++++ .../java/com/trilead/ssh2/SFTPException.java | 91 + .../java/com/trilead/ssh2/SFTPv3Client.java | 1389 +++++++++++++ .../trilead/ssh2/SFTPv3DirectoryEntry.java | 38 + .../trilead/ssh2/SFTPv3FileAttributes.java | 145 ++ .../com/trilead/ssh2/SFTPv3FileHandle.java | 45 + .../trilead/ssh2/ServerHostKeyVerifier.java | 31 + .../main/java/com/trilead/ssh2/Session.java | 529 +++++ .../java/com/trilead/ssh2/StreamGobbler.java | 229 +++ .../ssh2/auth/AuthenticationManager.java | 518 +++++ .../com/trilead/ssh2/auth/SignatureProxy.java | 52 + .../ssh2/channel/AuthAgentForwardThread.java | 605 ++++++ .../com/trilead/ssh2/channel/Channel.java | 207 ++ .../ssh2/channel/ChannelInputStream.java | 85 + .../trilead/ssh2/channel/ChannelManager.java | 1749 +++++++++++++++++ .../ssh2/channel/ChannelOutputStream.java | 71 + .../ssh2/channel/DynamicAcceptThread.java | 194 ++ .../ssh2/channel/IChannelWorkerThread.java | 13 + .../ssh2/channel/LocalAcceptThread.java | 135 ++ .../ssh2/channel/RemoteAcceptThread.java | 103 + .../ssh2/channel/RemoteForwardingData.java | 17 + .../ssh2/channel/RemoteX11AcceptThread.java | 247 +++ .../trilead/ssh2/channel/StreamForwarder.java | 111 ++ .../trilead/ssh2/channel/X11ServerData.java | 16 + .../ssh2/compression/CompressionFactory.java | 113 ++ .../trilead/ssh2/compression/ICompressor.java | 44 + .../com/trilead/ssh2/compression/Zlib.java | 142 ++ .../trilead/ssh2/compression/ZlibOpenSSH.java | 47 + .../java/com/trilead/ssh2/crypto/Base64.java | 148 ++ .../trilead/ssh2/crypto/CryptoWishList.java | 26 + .../com/trilead/ssh2/crypto/KeyMaterial.java | 91 + .../com/trilead/ssh2/crypto/PEMDecoder.java | 696 +++++++ .../com/trilead/ssh2/crypto/PEMStructure.java | 17 + .../trilead/ssh2/crypto/SimpleDERReader.java | 235 +++ .../com/trilead/ssh2/crypto/cipher/AES.java | 66 + .../ssh2/crypto/cipher/BlockCipher.java | 16 + .../crypto/cipher/BlockCipherFactory.java | 103 + .../trilead/ssh2/crypto/cipher/BlowFish.java | 435 ++++ .../trilead/ssh2/crypto/cipher/CBCMode.java | 78 + .../trilead/ssh2/crypto/cipher/CTRMode.java | 62 + .../ssh2/crypto/cipher/CipherInputStream.java | 133 ++ .../crypto/cipher/CipherOutputStream.java | 120 ++ .../com/trilead/ssh2/crypto/cipher/DES.java | 392 ++++ .../trilead/ssh2/crypto/cipher/DESede.java | 130 ++ .../trilead/ssh2/crypto/cipher/EtmCipher.java | 4 + .../ssh2/crypto/cipher/NullCipher.java | 35 + .../ssh2/crypto/dh/Curve25519Exchange.java | 85 + .../trilead/ssh2/crypto/dh/DhExchange.java | 212 ++ .../ssh2/crypto/dh/DhGroupExchange.java | 113 ++ .../trilead/ssh2/crypto/dh/EcDhExchange.java | 106 + .../ssh2/crypto/dh/GenericDhExchange.java | 96 + .../com/trilead/ssh2/crypto/digest/HMAC.java | 166 ++ .../ssh2/crypto/digest/HashForSSH2Types.java | 91 + .../com/trilead/ssh2/crypto/digest/MAC.java | 12 + .../com/trilead/ssh2/crypto/digest/MACs.java | 50 + .../ssh2/crypto/keys/Ed25519KeyFactory.java | 39 + .../crypto/keys/Ed25519KeyPairGenerator.java | 25 + .../ssh2/crypto/keys/Ed25519PrivateKey.java | 137 ++ .../ssh2/crypto/keys/Ed25519Provider.java | 42 + .../ssh2/crypto/keys/Ed25519PublicKey.java | 106 + .../java/com/trilead/ssh2/log/Logger.java | 54 + .../packets/PacketChannelAuthAgentReq.java | 33 + .../PacketChannelOpenConfirmation.java | 66 + .../packets/PacketChannelOpenFailure.java | 66 + .../packets/PacketChannelTrileadPing.java | 35 + .../packets/PacketChannelWindowAdjust.java | 57 + .../ssh2/packets/PacketDisconnect.java | 57 + .../trilead/ssh2/packets/PacketExtInfo.java | 76 + .../PacketGlobalCancelForwardRequest.java | 42 + .../packets/PacketGlobalForwardRequest.java | 41 + .../ssh2/packets/PacketGlobalTrileadPing.java | 32 + .../trilead/ssh2/packets/PacketIgnore.java | 59 + .../trilead/ssh2/packets/PacketKexDHInit.java | 31 + .../ssh2/packets/PacketKexDHReply.java | 53 + .../ssh2/packets/PacketKexDhGexGroup.java | 50 + .../ssh2/packets/PacketKexDhGexInit.java | 33 + .../ssh2/packets/PacketKexDhGexReply.java | 56 + .../ssh2/packets/PacketKexDhGexRequest.java | 39 + .../packets/PacketKexDhGexRequestOld.java | 34 + .../trilead/ssh2/packets/PacketKexInit.java | 165 ++ .../trilead/ssh2/packets/PacketNewKeys.java | 46 + .../packets/PacketOpenDirectTCPIPChannel.java | 56 + .../packets/PacketOpenSessionChannel.java | 62 + .../ssh2/packets/PacketServiceAccept.java | 61 + .../ssh2/packets/PacketServiceRequest.java | 52 + .../packets/PacketSessionExecCommand.java | 39 + .../ssh2/packets/PacketSessionPtyRequest.java | 57 + .../ssh2/packets/PacketSessionPtyResize.java | 40 + .../ssh2/packets/PacketSessionStartShell.java | 36 + .../PacketSessionSubsystemRequest.java | 40 + .../ssh2/packets/PacketSessionX11Request.java | 53 + .../ssh2/packets/PacketUserauthBanner.java | 60 + .../ssh2/packets/PacketUserauthFailure.java | 53 + .../packets/PacketUserauthInfoRequest.java | 84 + .../packets/PacketUserauthInfoResponse.java | 35 + .../PacketUserauthRequestInteractive.java | 43 + .../packets/PacketUserauthRequestNone.java | 61 + .../PacketUserauthRequestPassword.java | 67 + .../PacketUserauthRequestPublicKey.java | 65 + .../com/trilead/ssh2/packets/Packets.java | 153 ++ .../com/trilead/ssh2/packets/TypesReader.java | 192 ++ .../com/trilead/ssh2/packets/TypesWriter.java | 166 ++ .../com/trilead/ssh2/sftp/AttrTextHints.java | 38 + .../com/trilead/ssh2/sftp/AttribBits.java | 129 ++ .../com/trilead/ssh2/sftp/AttribFlags.java | 112 ++ .../trilead/ssh2/sftp/AttribPermissions.java | 32 + .../com/trilead/ssh2/sftp/AttribTypes.java | 28 + .../com/trilead/ssh2/sftp/ErrorCodes.java | 104 + .../java/com/trilead/ssh2/sftp/OpenFlags.java | 223 +++ .../java/com/trilead/ssh2/sftp/Packet.java | 43 + .../trilead/ssh2/signature/DSASHA1Verify.java | 253 +++ .../ssh2/signature/ECDSASHA2Verify.java | 582 ++++++ .../trilead/ssh2/signature/Ed25519Verify.java | 161 ++ .../trilead/ssh2/signature/RSASHA1Verify.java | 164 ++ .../ssh2/signature/RSASHA256Verify.java | 124 ++ .../ssh2/signature/RSASHA512Verify.java | 124 ++ .../trilead/ssh2/signature/SSHSignature.java | 23 + .../ssh2/transport/ClientServerHello.java | 127 ++ .../trilead/ssh2/transport/KexManager.java | 730 +++++++ .../trilead/ssh2/transport/KexParameters.java | 24 + .../com/trilead/ssh2/transport/KexState.java | 33 + .../ssh2/transport/MessageHandler.java | 14 + .../ssh2/transport/NegotiateException.java | 12 + .../ssh2/transport/NegotiatedParameters.java | 22 + .../ssh2/transport/TransportConnection.java | 364 ++++ .../ssh2/transport/TransportManager.java | 647 ++++++ .../com/trilead/ssh2/util/TimeoutService.java | 149 ++ .../java/com/trilead/ssh2/util/Tokenizer.java | 49 + .../rviz/common/base/BaseActivity.java | 285 +++ .../rviz/common/base/BaseAdapter.java | 115 ++ .../rviz/common/base/BaseDialog.java | 172 ++ .../rviz/common/base/BaseFragment.java | 102 + .../rviz/common/base/BaseService.java | 93 + .../rviz/common/base/BaseViewHolder.java | 26 + .../rviz/common/config/SSHAccountConfig.kt | 94 + .../rviz/common/coroutines/FlowBus.kt | 129 ++ .../rviz/common/db/BaseDao.java | 21 + .../rviz/common/utils/AESUtil.java | 53 + .../rviz/common/utils/DetectHtml.java | 41 + .../rviz/common/utils/LambdaTask.java | 27 + .../rviz/common/utils/NetworkUtilsExtend.kt | 127 ++ .../rviz/common/utils/PermissionUtil.java | 434 ++++ .../rviz/common/utils/ToastUtil.java | 38 + .../rviz/common/utils/Utils.java | 57 + .../rviz/constant/AppConfigInfo.kt | 23 + .../rviz/constant/DiagnoseSource.kt | 13 + .../rviz/constant/DiagnoseType.kt | 5 + .../rviz/constant/EventKey.kt | 23 + .../rviz/constant/FaultLevel.kt | 123 ++ .../rviz/constant/FaultModuleId.kt | 5 + .../rviz/constant/FaultSubModuleId.kt | 147 ++ .../rviz/constant/MsgFmData.kt | 164 ++ .../rviz/constant/SensorCamera.kt | 14 + .../rviz/dialog/ChangeDefaultConfigDialog.kt | 111 ++ .../rviz/dialog/CommonLoadingDialog.kt | 66 + .../rviz/dialog/DockersDialog.java | 91 + .../rviz/dialog/FMDataShowDialog.kt | 76 + .../rviz/dialog/FaultCodeDetailsDialog.kt | 357 ++++ .../rviz/dialog/InputUserPwdDialog.kt | 101 + .../rviz/dialog/ShowConfigDialog.java | 84 + .../rviz/dialog/StartupConfigDialog.java | 157 ++ .../dialog/adapter/DockerListAdapter.java | 79 + .../dialog/adapter/FaultCodeDetailsAdapter.kt | 153 ++ .../dialog/adapter/StartupConfigAdapter.java | 119 ++ .../rviz/model/db/CarStatusDao.java | 15 + .../rviz/model/db/DataStorage.java | 63 + .../rviz/model/db/FmCodeDao.java | 33 + .../rviz/model/db/FmCodeRepository.java | 30 + .../model/entities/AdasConnectionStatus.kt | 5 + .../rviz/model/entities/CarStatusEntity.java | 28 + .../rviz/model/entities/DiagnoseInfo.kt | 24 + .../rviz/model/entities/DiskInfo.java | 13 + .../rviz/model/entities/DockerBean.java | 15 + .../model/entities/DockerConfigContent.java | 15 + .../rviz/model/entities/DockerInfo.java | 184 ++ .../rviz/model/entities/DockerStatus.java | 13 + .../rviz/model/entities/FMInfoMsg.kt | 22 + .../model/entities/FaultCodeDetailsInfo.kt | 5 + .../rviz/model/entities/FaultCodeEntity.java | 28 + .../rviz/model/entities/FmCodeEntity.java | 51 + .../rviz/model/entities/FmEntity.kt | 12 + .../rviz/model/entities/HdMapVersion.kt | 8 + .../rviz/model/entities/ModuleStatusEntity.kt | 22 + .../rviz/model/entities/NodeHealthEntity.java | 25 + .../rviz/model/entities/RosHostArgument.java | 276 +++ .../rviz/model/entities/SensorStatusEntity.kt | 13 + .../rviz/model/entities/StartupConfig.java | 75 + .../rviz/model/entities/SystemLogEntity.java | 26 + .../model/entities/SystemResourceEntity.java | 28 + .../rviz/model/entities/TabEntity.java | 30 + .../rviz/model/entities/VehicleConfig.java | 86 + .../rviz/net/BaseResponse.kt | 5 + .../rviz/net/FmdNetManager.kt | 47 + .../rviz/net/HostConst.kt | 22 + .../rviz/net/MoGoRetrofitFactory.java | 19 + .../rviz/net/NetworkCallback.kt | 6 + .../rviz/net/api/FMdNetModel.kt | 73 + .../rviz/net/api/FmdApi.kt | 23 + .../api/callback/CarInfoByParamCallback.kt | 14 + .../net/api/entity/CarInfoByParamResponse.kt | 38 + .../rviz/net/api/entity/Result.kt | 21 + .../FaultManagementDiagnosisService.kt | 1203 ++++++++++++ .../rviz/service/FmCodeUpdateService.kt | 104 + .../rviz/ssh/SSH.java | 399 ++++ .../rviz/ssh/constant/MogoCommand.java | 50 + .../rviz/ssh/constant/SSHConstant.java | 9 + .../CallerSshConnectionListenerManager.kt | 62 + .../listener/OnDockerExecCommandListener.java | 20 + .../listener/OnExecCommandListener.java | 9 + .../listener/OnSshConnectionListener.java | 43 + .../rviz/ssh/module/DockerCommandHandler.java | 408 ++++ .../rviz/ssh/module/ExecCommandHandler.java | 147 ++ .../rviz/ssh/module/SSHHostBean.java | 304 +++ .../rviz/ui/activity/FmdAct.kt | 335 ++++ .../rviz/ui/fragments/FmdBaseFragment.kt | 21 + .../ui/fragments/fault/FaultCodeAdapter.kt | 136 ++ .../rviz/ui/fragments/fault/FaultCodeFrag.kt | 138 ++ .../overview/ModuleStatusAdapter.java | 61 + .../ui/fragments/overview/OverviewFrag.kt | 884 +++++++++ .../fragments/resource/SystemResourceFrag.kt | 245 +++ .../rviz/ui/views/ColorHintFloatWindow.java | 119 ++ .../ui/views/ColorHintFloatWindowManager.java | 95 + .../rviz/ui/views/SensorStatusView.kt | 206 ++ .../rviz/ui/views/StateBarView.java | 248 +++ .../rviz/widgets/CustomCheckBox.java | 23 + .../rviz/widgets/FmdProgressBar.java | 158 ++ .../rviz/widgets/JustifiedTextView.kt | 47 + .../rviz/widgets/MyLinearLayoutManager.java | 37 + .../ros/host/OnRosHostClickListener.java | 16 + .../rviz/widgets/ros/host/RosHostAdapter.java | 222 +++ .../rviz/widgets/ros/host/RosHostView.java | 256 +++ .../res/color/rviz_fmd_selector_txt_color.xml | 5 + .../drawable-hdpi/rviz_fmd_icon_camera.png | Bin 0 -> 2917 bytes .../rviz_fmd_icon_car_chassis.png | Bin 0 -> 1946 bytes .../rviz_fmd_icon_hd_map_version.png | Bin 0 -> 3053 bytes .../res/drawable-hdpi/rviz_fmd_icon_ipc.png | Bin 0 -> 841 bytes .../drawable-hdpi/rviz_fmd_icon_ipc_error.png | Bin 0 -> 826 bytes .../rviz_fmd_icon_laser_radar.png | Bin 0 -> 3380 bytes .../rviz_fmd_icon_launcher_eagle.png | Bin 0 -> 3254 bytes .../rviz_fmd_icon_map_verson.png | Bin 0 -> 2075 bytes .../rviz_fmd_icon_millimeter_wave_radar.png | Bin 0 -> 2920 bytes .../res/drawable-hdpi/rviz_fmd_icon_rtk.png | Bin 0 -> 2871 bytes .../rviz_fmd_tab_car_status_select.png | Bin 0 -> 1327 bytes .../rviz_fmd_tab_car_status_unselect.png | Bin 0 -> 1197 bytes .../rviz_fmd_tab_fault_code_select.png | Bin 0 -> 1680 bytes .../rviz_fmd_tab_fault_code_unselect.png | Bin 0 -> 1540 bytes .../rviz_fmd_tab_system_resource_select.png | Bin 0 -> 702 bytes .../rviz_fmd_tab_system_resource_unselect.png | Bin 0 -> 628 bytes .../drawable-mdpi/rviz_fmd_icon_camera.png | Bin 0 -> 1952 bytes .../rviz_fmd_icon_car_chassis.png | Bin 0 -> 1326 bytes .../rviz_fmd_icon_hd_map_version.png | Bin 0 -> 1883 bytes .../res/drawable-mdpi/rviz_fmd_icon_ipc.png | Bin 0 -> 511 bytes .../drawable-mdpi/rviz_fmd_icon_ipc_error.png | Bin 0 -> 514 bytes .../rviz_fmd_icon_laser_radar.png | Bin 0 -> 2236 bytes .../rviz_fmd_icon_launcher_eagle.png | Bin 0 -> 1786 bytes .../rviz_fmd_icon_map_verson.png | Bin 0 -> 1562 bytes .../res/drawable-mdpi/rviz_fmd_icon_rtk.png | Bin 0 -> 1884 bytes .../rviz_fmd_tab_car_status_select.png | Bin 0 -> 906 bytes .../rviz_fmd_tab_car_status_unselect.png | Bin 0 -> 790 bytes .../rviz_fmd_tab_fault_code_select.png | Bin 0 -> 1198 bytes .../rviz_fmd_tab_fault_code_unselect.png | Bin 0 -> 1052 bytes .../rviz_fmd_tab_system_resource_select.png | Bin 0 -> 503 bytes .../rviz_fmd_tab_system_resource_unselect.png | Bin 0 -> 435 bytes .../drawable-xhdpi/rviz_fmd_icon_camera.png | Bin 0 -> 3882 bytes .../rviz_fmd_icon_car_chassis.png | Bin 0 -> 2483 bytes .../rviz_fmd_icon_hd_map_version.png | Bin 0 -> 3961 bytes .../res/drawable-xhdpi/rviz_fmd_icon_ipc.png | Bin 0 -> 884 bytes .../rviz_fmd_icon_ipc_error.png | Bin 0 -> 875 bytes .../rviz_fmd_icon_laser_radar.png | Bin 0 -> 4551 bytes .../rviz_fmd_icon_launcher_eagle.png | Bin 0 -> 5325 bytes .../rviz_fmd_icon_map_verson.png | Bin 0 -> 2836 bytes .../res/drawable-xhdpi/rviz_fmd_icon_rtk.png | Bin 0 -> 3854 bytes .../rviz_fmd_tab_car_status_select.png | Bin 0 -> 1668 bytes .../rviz_fmd_tab_car_status_unselect.png | Bin 0 -> 1522 bytes .../rviz_fmd_tab_fault_code_select.png | Bin 0 -> 2127 bytes .../rviz_fmd_tab_fault_code_unselect.png | Bin 0 -> 1988 bytes .../rviz_fmd_tab_system_resource_select.png | Bin 0 -> 718 bytes .../rviz_fmd_tab_system_resource_unselect.png | Bin 0 -> 674 bytes .../drawable-xxhdpi/rviz_fmd_icon_camera.png | Bin 0 -> 13940 bytes .../rviz_fmd_icon_car_chassis.png | Bin 0 -> 4976 bytes .../rviz_fmd_icon_hd_map_version.png | Bin 0 -> 5511 bytes .../res/drawable-xxhdpi/rviz_fmd_icon_ipc.png | Bin 0 -> 1510 bytes .../rviz_fmd_icon_ipc_error.png | Bin 0 -> 1447 bytes .../rviz_fmd_icon_laser_radar.png | Bin 0 -> 16561 bytes .../rviz_fmd_icon_launcher_eagle.png | Bin 0 -> 11082 bytes .../rviz_fmd_icon_map_verson.png | Bin 0 -> 4008 bytes .../res/drawable-xxhdpi/rviz_fmd_icon_rtk.png | Bin 0 -> 13828 bytes .../rviz_fmd_tab_car_status_select.png | Bin 0 -> 2466 bytes .../rviz_fmd_tab_car_status_unselect.png | Bin 0 -> 2380 bytes .../rviz_fmd_tab_fault_code_select.png | Bin 0 -> 3207 bytes .../rviz_fmd_tab_fault_code_unselect.png | Bin 0 -> 3043 bytes .../rviz_fmd_tab_system_resource_select.png | Bin 0 -> 1137 bytes .../rviz_fmd_tab_system_resource_unselect.png | Bin 0 -> 1111 bytes .../res/drawable/rviz_common_bg_dialog.xml | 14 + .../rviz_common_bg_dockers_item_header.xml | 16 + .../rviz_common_bg_fmd_dialog_btn.xml | 29 + ..._common_dialog_default_config_input_bg.xml | 21 + ...select_dialog_default_config_button_bg.xml | 15 + .../rviz_fmd_bg_close_flow_button.xml | 5 + .../rviz_fmd_bg_color_hint_button.xml | 25 + .../res/drawable/rviz_fmd_bg_dialog_btn.xml | 29 + .../rviz_fmd_bg_dialog_fault_code_details.xml | 10 + ..._fmd_bg_dialog_fault_code_details_item.xml | 20 + .../main/res/drawable/rviz_fmd_bg_fm_btn.xml | 19 + .../rviz_fmd_bg_fm_data_show_data.xml | 20 + .../drawable/rviz_fmd_bg_fmd_dialog_btn.xml | 29 + .../res/drawable/rviz_fmd_bg_item_btn.xml | 20 + .../rviz_fmd_bg_item_btn_reconnect.xml | 20 + .../rviz_fmd_bg_item_dockers_even.xml | 17 + .../drawable/rviz_fmd_bg_item_dockers_odd.xml | 17 + .../res/drawable/rviz_fmd_bg_item_error.xml | 10 + .../res/drawable/rviz_fmd_bg_item_host.xml | 14 + .../rviz_fmd_bg_item_host_loading.xml | 17 + .../res/drawable/rviz_fmd_bg_item_normal.xml | 10 + .../drawable/rviz_fmd_bg_ros_host_view.xml | 14 + .../rviz_fmd_bg_show_config_value.xml | 18 + ...viz_fmd_dialog_default_config_input_bg.xml | 21 + .../drawable/rviz_fmd_ic_connect_failed.png | Bin 0 -> 2743 bytes .../res/drawable/rviz_fmd_ic_disconnected.png | Bin 0 -> 1503 bytes .../rviz_fmd_ic_fault_code_normal.xml | 13 + .../rviz_fmd_ic_fault_code_unknown.xml | 9 + ...rviz_fmd_ic_group_indicator_expanded.9.png | Bin 0 -> 412 bytes ...iz_fmd_ic_group_indicator_unexpanded.9.png | Bin 0 -> 352 bytes .../main/res/drawable/rviz_fmd_icon_error.xml | 12 + .../res/drawable/rviz_fmd_icon_normal.xml | 12 + .../res/drawable/rviz_fmd_progress_bar.xml | 36 + .../drawable/rviz_fmd_selector_fault_code.xml | 5 + .../rviz_fmd_selector_group_indicator.xml | 5 + .../drawable/rviz_fmd_selector_item_bg.xml | 5 + .../rviz_fmd_selector_status_icon.xml | 6 + .../drawable/rviz_fmd_selector_text_color.xml | 8 + .../drawable/rvzi_fmd_bg_color_hint_float.xml | 6 + .../rviz_common_dialog_default_config.xml | 198 ++ .../src/main/res/layout/rviz_fmd_act_home.xml | 79 + .../layout/rviz_fmd_dialog_common_loading.xml | 30 + .../res/layout/rviz_fmd_dialog_dockers.xml | 83 + .../rviz_fmd_dialog_fault_code_details.xml | 120 ++ .../layout/rviz_fmd_dialog_fm_data_show.xml | 63 + .../layout/rviz_fmd_dialog_fmd_user_pwd.xml | 155 ++ .../layout/rviz_fmd_dialog_show_config.xml | 59 + .../res/layout/rviz_fmd_frag_fault_code.xml | 41 + .../res/layout/rviz_fmd_frag_overview.xml | 100 + .../layout/rviz_fmd_frag_system_resource.xml | 5 + ...d_item_car_status_module_sensor_status.xml | 48 + ...rviz_fmd_item_child_fault_code_details.xml | 184 ++ .../res/layout/rviz_fmd_item_color_hint.xml | 21 + .../main/res/layout/rviz_fmd_item_dockers.xml | 60 + .../res/layout/rviz_fmd_item_fault_code.xml | 111 ++ ...rviz_fmd_item_group_fault_code_details.xml | 37 + .../src/main/res/layout/rviz_fmd_item_ros.xml | 260 +++ .../layout/rviz_fmd_item_startup_config.xml | 83 + .../res/layout/rviz_fmd_view_progress_bar.xml | 35 + .../res/layout/rviz_fmd_view_ros_host.xml | 46 + .../res/layout/rviz_fmd_view_state_bar.xml | 79 + .../src/main/res/values/attrs.xml | 11 + .../src/main/res/values/color.xml | 20 + .../src/main/res/values/strings.xml | 19 + .../src/main/res/values/styles.xml | 77 + .../mogo-core-function-hmi/build.gradle | 1 + .../hmi/ui/setting/DebugSettingView.kt | 44 +- .../main/res/layout/view_debug_setting.xml | 32 +- ...llerFaultManagementStateListenerManager.kt | 8 + settings.gradle | 1 + 388 files changed, 36083 insertions(+), 7 deletions(-) create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/.gitignore create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/build.gradle create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/gradle.properties create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/proguard-rules.pro create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/AndroidManifest.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db-shm create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db-wal create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/AuthAgentCallback.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ChannelCondition.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Connection.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionInfo.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionMonitor.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DHGexParameters.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DebugLogger.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtendedServerHostKeyVerifier.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtensionInfo.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyData.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyException.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/InteractiveCallback.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/KnownHosts.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalPortForwarder.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ProxyData.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SCPClient.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPException.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3Client.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Session.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/StreamGobbler.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/SignatureProxy.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/Channel.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelManager.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/X11ServerData.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ICompressor.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/Zlib.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/Base64.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/EtmCipher.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HMAC.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MACs.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactory.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyPairGenerator.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519Provider.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/log/Logger.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketExtInfo.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/Packets.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesReader.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesWriter.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribBits.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/Packet.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/Ed25519Verify.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA256Verify.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA512Verify.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/SSHSignature.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexManager.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexParameters.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexState.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/MessageHandler.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiateException.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportConnection.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportManager.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/TimeoutService.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/Tokenizer.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseActivity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseAdapter.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseDialog.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseFragment.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseService.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseViewHolder.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/config/SSHAccountConfig.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/coroutines/FlowBus.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/db/BaseDao.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/AESUtil.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/DetectHtml.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/LambdaTask.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/NetworkUtilsExtend.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/PermissionUtil.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/ToastUtil.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/Utils.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/AppConfigInfo.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseSource.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseType.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/EventKey.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultLevel.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultModuleId.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultSubModuleId.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/MsgFmData.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/SensorCamera.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ChangeDefaultConfigDialog.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/CommonLoadingDialog.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/DockersDialog.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FMDataShowDialog.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FaultCodeDetailsDialog.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/InputUserPwdDialog.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ShowConfigDialog.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/StartupConfigDialog.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/DockerListAdapter.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/FaultCodeDetailsAdapter.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/StartupConfigAdapter.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/CarStatusDao.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/DataStorage.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeDao.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeRepository.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/AdasConnectionStatus.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/CarStatusEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiagnoseInfo.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiskInfo.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerBean.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerConfigContent.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerInfo.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerStatus.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FMInfoMsg.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeDetailsInfo.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmCodeEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmEntity.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/HdMapVersion.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/ModuleStatusEntity.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/NodeHealthEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/RosHostArgument.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SensorStatusEntity.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/StartupConfig.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemLogEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemResourceEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/TabEntity.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/VehicleConfig.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/BaseResponse.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/FmdNetManager.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/HostConst.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/MoGoRetrofitFactory.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/NetworkCallback.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FMdNetModel.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FmdApi.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/callback/CarInfoByParamCallback.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/CarInfoByParamResponse.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/Result.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FaultManagementDiagnosisService.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FmCodeUpdateService.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/SSH.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/MogoCommand.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/SSHConstant.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/call/CallerSshConnectionListenerManager.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnDockerExecCommandListener.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnExecCommandListener.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnSshConnectionListener.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/DockerCommandHandler.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/ExecCommandHandler.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/SSHHostBean.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/activity/FmdAct.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/FmdBaseFragment.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeAdapter.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeFrag.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/ModuleStatusAdapter.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/OverviewFrag.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/resource/SystemResourceFrag.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindow.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindowManager.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/SensorStatusView.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/StateBarView.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/CustomCheckBox.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/FmdProgressBar.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/JustifiedTextView.kt create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/MyLinearLayoutManager.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/OnRosHostClickListener.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostAdapter.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostView.java create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/color/rviz_fmd_selector_txt_color.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_camera.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_car_chassis.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_hd_map_version.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_ipc.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_ipc_error.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_laser_radar.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_launcher_eagle.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_map_verson.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_millimeter_wave_radar.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_rtk.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_car_status_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_car_status_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_fault_code_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_fault_code_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_system_resource_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_system_resource_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_camera.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_car_chassis.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_hd_map_version.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_ipc.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_ipc_error.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_laser_radar.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_launcher_eagle.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_map_verson.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_rtk.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_car_status_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_car_status_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_fault_code_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_fault_code_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_system_resource_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_system_resource_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_camera.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_car_chassis.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_hd_map_version.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_ipc.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_ipc_error.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_laser_radar.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_launcher_eagle.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_map_verson.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_rtk.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_car_status_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_car_status_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_fault_code_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_fault_code_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_system_resource_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_system_resource_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_camera.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_car_chassis.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_hd_map_version.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_ipc.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_ipc_error.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_laser_radar.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_launcher_eagle.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_map_verson.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_rtk.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_car_status_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_car_status_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_fault_code_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_fault_code_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_system_resource_select.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_system_resource_unselect.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dialog.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dockers_item_header.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_fmd_dialog_btn.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_dialog_default_config_input_bg.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_select_dialog_default_config_button_bg.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_close_flow_button.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_color_hint_button.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_btn.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details_item.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_btn.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_data_show_data.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fmd_dialog_btn.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn_reconnect.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_even.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_odd.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_error.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host_loading.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_normal.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_ros_host_view.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_show_config_value.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_dialog_default_config_input_bg.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_connect_failed.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_disconnected.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_normal.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_unknown.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_group_indicator_expanded.9.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_group_indicator_unexpanded.9.png create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_icon_error.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_icon_normal.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_progress_bar.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_fault_code.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_group_indicator.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_item_bg.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_status_icon.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_text_color.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rvzi_fmd_bg_color_hint_float.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_common_dialog_default_config.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_act_home.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_dialog_common_loading.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_dialog_dockers.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_dialog_fault_code_details.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_dialog_fm_data_show.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_dialog_fmd_user_pwd.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_dialog_show_config.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_frag_fault_code.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_frag_overview.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_frag_system_resource.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_car_status_module_sensor_status.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_child_fault_code_details.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_color_hint.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_dockers.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_fault_code.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_group_fault_code_details.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_ros.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_item_startup_config.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_view_progress_bar.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_view_ros_host.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_fmd_view_state_bar.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/values/attrs.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/values/color.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/values/strings.xml create mode 100644 core/function-impl/mogo-core-function-devatools-rviz/src/main/res/values/styles.xml diff --git a/core/function-impl/mogo-core-function-devatools-rviz/.gitignore b/core/function-impl/mogo-core-function-devatools-rviz/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/build.gradle b/core/function-impl/mogo-core-function-devatools-rviz/build.gradle new file mode 100644 index 0000000000..6bee5136ce --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/build.gradle @@ -0,0 +1,92 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-android-extensions' + id 'kotlin-kapt' +} + +android { + compileSdkVersion rootProject.ext.android.compileSdkVersion + defaultConfig { + minSdkVersion rootProject.ext.android.minSdkVersion + targetSdkVersion rootProject.ext.android.targetSdkVersion + versionCode Integer.valueOf(VERSION_CODE) + versionName getValueFromRootProperties("${project.name.replace("-", "_").toUpperCase()}_VERSION") + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + } + + //排除包中的proto文件 + packagingOptions { + exclude '**/*.proto' + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += [ + "-Xopt-in=kotlin.RequiresOptIn" + ] + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation rootProject.ext.dependencies.kotlinstdlib + implementation rootProject.ext.dependencies.mogologlib + + implementation rootProject.ext.dependencies.androidx_datastore + implementation rootProject.ext.dependencies.androidxroomruntime + kapt rootProject.ext.dependencies.androidxroomcompiler + implementation rootProject.ext.dependencies.rxandroid + implementation rootProject.ext.dependencies.androidxcardview + implementation rootProject.ext.dependencies.androidxroomktx + implementation rootProject.ext.dependencies.androidxappcompat + implementation rootProject.ext.dependencies.androidxconstraintlayout + implementation rootProject.ext.dependencies.androidxrecyclerview + + implementation rootProject.ext.dependencies.life_cycle_scope + + implementation project(':core:mogo-core-function-call') + implementation project(':core:mogo-core-function-api') + implementation project(':core:mogo-core-res') + implementation project(':foudations:mogo-commons') + implementation 'io.github.h07000223:flycoTabLayout:3.0.0' + implementation project(':core:mogo-core-utils') + implementation project(':libraries:mogo-adas') + implementation project(':libraries:mogo-adas-data') + + //----------------SSH start--------------------- + //implementation('org.connectbot:sshlib:2.2.21') + //正常引用 org.connectbot:sshlib:2.2.21库由于存在冲突 故将原始代码拷贝到项目中并引用库中的第三方库 + implementation("com.jcraft:jzlib:1.1.3") + implementation("org.connectbot:simplesocks:1.0.1") + implementation("com.google.crypto.tink:tink:1.7.0") + implementation("org.connectbot:jbcrypt:1.0.2") + //----------------SSH end--------------------- + implementation 'org.conscrypt:conscrypt-android:2.5.2' +} + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/gradle.properties b/core/function-impl/mogo-core-function-devatools-rviz/gradle.properties new file mode 100644 index 0000000000..768f0ce1ac --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/gradle.properties @@ -0,0 +1,3 @@ +GROUP=com.mogo.eagle.core.function.impl +POM_ARTIFACT_ID=devatools_rviz +VERSION_CODE=1 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/proguard-rules.pro b/core/function-impl/mogo-core-function-devatools-rviz/proguard-rules.pro new file mode 100644 index 0000000000..3dbb226707 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/proguard-rules.pro @@ -0,0 +1,23 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-dontwarn android.arch.persistence.** \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/AndroidManifest.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..7d1e9d6a3e --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db b/core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db new file mode 100644 index 0000000000000000000000000000000000000000..b3cc78585e940e1acfa4c80cd4e2fbd9acbc9218 GIT binary patch literal 397312 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

O+7=K6(wp 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 H*yjCzartSx literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db-shm b/core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..60c4f90f2866646ec118e1980347b6a90905bc0d GIT binary patch literal 32768 zcmeI*y{Q605CGuy-v|2~0As(;_5-Izcj5dT*c1PBlyK!5-N0t5&UAV7csf!73T x#k&r#t+k2(0RjXF5FkK+009C72oNYE5N{(20t5&UAV7cs0RjXF5Fk)S-~ly>Enolu literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db-wal b/core/function-impl/mogo-core-function-devatools-rviz/src/main/assets/AutoPilotVisualDB.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..813a6f51a58e41e36ae8e3bef426997b6a3c2e52 GIT binary patch literal 432632 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 aOm8gjNZ retrieveIdentities(); + + /** + * @param pair A RSAPrivateKey, ECPrivateKey, or + * DSAPrivateKey containing a DSA, EC, or RSA private + * and corresponding PublicKey. + * @param comment comment associated with this key + * @param confirmUse whether to prompt before using this key + * @param lifetime lifetime in seconds for key to be remembered + * @return success or failure + */ + boolean addIdentity(KeyPair pair, String comment, boolean confirmUse, int lifetime); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return success or failure + */ + boolean removeIdentity(byte[] publicKey); + + /** + * @return success or failure + */ + boolean removeAllIdentities(); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return A RSAPrivateKey or DSAPrivateKey + * containing a DSA or RSA private key of + * the user in Trilead object format. + */ + KeyPair getKeyPair(byte[] publicKey); + + /** + * @return + */ + boolean isAgentLocked(); + + /** + * @param lockPassphrase + */ + boolean setAgentLock(String lockPassphrase); + + /** + * @param unlockPassphrase + * @return + */ + boolean requestAgentUnlock(String unlockPassphrase); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ChannelCondition.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ChannelCondition.java new file mode 100644 index 0000000000..ef8a5aa3e8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ChannelCondition.java @@ -0,0 +1,61 @@ + +package com.trilead.ssh2; + +/** + * Contains constants that can be used to specify what conditions to wait for on + * a SSH-2 channel (e.g., represented by a {@link Session}). + * + * @see Session#waitForCondition(int, long) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelCondition.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface ChannelCondition +{ + /** + * A timeout has occurred, none of your requested conditions is fulfilled. + * However, other conditions may be true - therefore, NEVER use the "==" + * operator to test for this (or any other) condition. Always use + * something like ((cond & ChannelCondition.CLOSED) != 0). + */ + int TIMEOUT = 1; + + /** + * The underlying SSH-2 channel, however not necessarily the whole connection, + * has been closed. This implies EOF. Note that there may still + * be unread stdout or stderr data in the local window, i.e, STDOUT_DATA + * or/and STDERR_DATA may be set at the same time. + */ + int CLOSED = 2; + + /** + * There is stdout data available that is ready to be consumed. + */ + int STDOUT_DATA = 4; + + /** + * There is stderr data available that is ready to be consumed. + */ + int STDERR_DATA = 8; + + /** + * EOF on has been reached, no more _new_ stdout or stderr data will arrive + * from the remote server. However, there may be unread stdout or stderr + * data, i.e, STDOUT_DATA or/and STDERR_DATA + * may be set at the same time. + */ + int EOF = 16; + + /** + * The exit status of the remote process is available. + * Some servers never send the exist status, or occasionally "forget" to do so. + */ + int EXIT_STATUS = 32; + + /** + * The exit signal of the remote process is available. + */ + int EXIT_SIGNAL = 64; + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Connection.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Connection.java new file mode 100644 index 0000000000..64e3fe0d45 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Connection.java @@ -0,0 +1,1564 @@ + +package com.trilead.ssh2; + +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.util.Vector; + +import com.trilead.ssh2.auth.AuthenticationManager; +import com.trilead.ssh2.auth.SignatureProxy; +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.digest.MACs; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketIgnore; +import com.trilead.ssh2.transport.KexManager; +import com.trilead.ssh2.transport.TransportManager; +import com.trilead.ssh2.util.TimeoutService; +import com.trilead.ssh2.util.TimeoutService.TimeoutToken; + +/** + * A Connection is used to establish an encrypted TCP/IP + * connection to a SSH-2 server. + *

+ * Typically, one + *

    + *
  1. creates a {@link #Connection(String) Connection} object.
  2. + *
  3. calls the {@link #connect() connect()} method.
  4. + *
  5. calls some of the authentication methods (e.g., + * {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()}).
  6. + *
  7. calls one or several times the {@link #openSession() openSession()} + * method.
  8. + *
  9. finally, one must close the connection and release resources with the + * {@link #close() close()} method.
  10. + *
+ * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Connection.java,v 1.3 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class Connection implements AutoCloseable +{ + /** + * The identifier presented to the SSH-2 server. + */ + public final static String identification = "TrileadSSH2Java_213"; + + /** + * Will be used to generate all random data needed for the current + * connection. Note: SecureRandom.nextBytes() is thread safe. + */ + private SecureRandom generator; + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported cipher algorithms by this implementation. + */ + public static synchronized String[] getAvailableCiphers() + { + return BlockCipherFactory.getDefaultCipherList(); + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported MAC algorthims by this implementation. + */ + public static synchronized String[] getAvailableMACs() + { + return MACs.getMacList(); + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported server host key algorthims by this + * implementation. + */ + public static synchronized String[] getAvailableServerHostKeyAlgorithms() + { + return KexManager.getDefaultServerHostkeyAlgorithmList(); + } + + private AuthenticationManager am; + + private boolean authenticated = false; + private boolean compression = false; + private ChannelManager cm; + + private CryptoWishList cryptoWishList = new CryptoWishList(); + + private DHGexParameters dhgexpara = new DHGexParameters(); + + private final String hostname; + + private final int port; + + private TransportManager tm; + + private ProxyData proxyData = null; + + private Vector connectionMonitors = new Vector(); + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + *

+ * Same as {@link #Connection(String, int) Connection(hostname, 22)}. + * + * @param hostname + * the hostname of the SSH-2 server. + */ + public Connection(String hostname) + { + this(hostname, 22); + } + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + * + * @param hostname + * the host where we later want to connect to. + * @param port + * port on the server, normally 22. + */ + public Connection(String hostname, int port) + { + this.hostname = hostname; + this.port = port; + } + + /** + * A wrapper that calls + * {@link #authenticateWithKeyboardInteractive(String, String[], InteractiveCallback) + * authenticateWithKeyboardInteractivewith} a null submethod + * list. + * + * @param user + * A String holding the username. + * @param cb + * An InteractiveCallback which will be used to + * determine the responses to the questions asked by the server. + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithKeyboardInteractive(String user, InteractiveCallback cb) + throws IOException + { + return authenticateWithKeyboardInteractive(user, null, cb); + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * is based on "keyboard-interactive", specified in + * draft-ietf-secsh-auth-kbdinteract-XX. Basically, you have to define a + * callback object which will be feeded with challenges generated by the + * server. Answers are then sent back to the server. It is possible that the + * callback will be called several times during the invocation of this + * method (e.g., if the server replies to the callback's answer(s) with + * another challenge...) + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * Note: some SSH servers advertise "keyboard-interactive", however, any + * interactive request will be denied (without having sent any challenge to + * the client). + * + * @param user + * A String holding the username. + * @param submethods + * An array of submethod names, see + * draft-ietf-secsh-auth-kbdinteract-XX. May be null + * to indicate an empty list. + * @param cb + * An InteractiveCallback which will be used to + * determine the responses to the questions asked by the server. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithKeyboardInteractive(String user, String[] submethods, + InteractiveCallback cb) throws IOException + { + if (cb == null) + throw new IllegalArgumentException("Callback may not ne NULL!"); + + checkRequirements(user); + + authenticated = am.authenticateInteractive(user, submethods, cb); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * sends username and password to the server. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * Note: if this method fails, then please double-check that it is actually + * offered by the server (use + * {@link #getRemainingAuthMethods(String) getRemainingAuthMethods()}. + *

+ * Often, password authentication is disabled, but users are not aware of + * it. Many servers only offer "publickey" and "keyboard-interactive". + * However, even though "keyboard-interactive" *feels* like password + * authentication (e.g., when using the putty or openssh clients) it is + * *not* the same mechanism. + * + * @param user + * @param password + * @return if the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPassword(String user, String password) throws IOException + { + if (password == null) + throw new IllegalArgumentException("password argument is null"); + + checkRequirements(user); + + authenticated = am.authenticatePassword(user, password); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * can be used to explicitly use the special "none" authentication method + * (where only a username has to be specified). + *

+ * Note 1: The "none" method may always be tried by clients, however as by + * the specs, the server will not explicitly announce it. In other words, + * the "none" token will never show up in the list returned by + * {@link #getRemainingAuthMethods(String)}. + *

+ * Note 2: no matter which one of the authenticateWithXXX() methods you + * call, the library will always issue exactly one initial "none" + * authentication request to retrieve the initially allowed list of + * authentication methods by the server. Please read RFC 4252 for the + * details. + *

+ * If the authentication phase is complete, true will be + * returned. If further authentication steps are needed, false + * is returned and one can retry by any other authentication method (use the + * getRemainingAuthMethods method to get a list of the + * remaining possible methods). + * + * @param user the username to attempt to log in as + * @return if the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithNone(String user) throws IOException + { + checkRequirements(user); + + /* Trigger the sending of the PacketUserauthRequestNone packet */ + /* (if not already done) */ + + authenticated = am.authenticateNone(user); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. The + * authentication method "publickey" works by signing a challenge sent by + * the server. The signature is either DSA, EC, or RSA based - it just depends + * on the type of private key you specify, either a DSA or RSA private key in + * PEM format. And yes, this is may seem to be a little confusing, the + * method is called "publickey" in the SSH-2 protocol specification, however + * since we need to generate a signature, you actually have to supply a + * private key =). + *

+ * The private key contained in the PEM file may also be encrypted + * ("Proc-Type: 4,ENCRYPTED"). The library supports DES-CBC and DES-EDE3-CBC + * encryption, as well as the more exotic PEM encryption AES-128-CBC, + * AES-192-CBC and AES-256-CBC. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * NOTE PUTTY USERS: Event though your key file may start with + * "-----BEGIN..." it is not in the expected format. You have to convert it + * to the OpenSSH key format by using the "puttygen" tool (can be downloaded + * from the Putty website). Simply load your key and then use the + * "Conversions/Export OpenSSH key" functionality to get a proper PEM file. + * + * @param user + * A String holding the username. + * @param pemPrivateKey + * A char[] containing a DSA or RSA private key of + * the user in OpenSSH key format (PEM, you can't miss the + * "-----BEGIN DSA PRIVATE KEY-----" or "-----BEGIN RSA PRIVATE + * KEY-----" tag). The char array may contain + * linebreaks/linefeeds. + * @param password + * If the PEM structure is encrypted ("Proc-Type: 4,ENCRYPTED") + * then you must specify a password. Otherwise, this argument + * will be ignored and can be set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, char[] pemPrivateKey, String password) + throws IOException + { + if (pemPrivateKey == null) + throw new IllegalArgumentException("pemPrivateKey argument is null"); + + checkRequirements(user); + + authenticated = am.authenticatePublicKey(user, pemPrivateKey, password, getOrCreateSecureRND()); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. The + * authentication method "publickey" works by signing a challenge sent by + * the server. The signature is either DSA, EC, or RSA based - it just depends + * on the type of private key you specify, either a DSA, EC, or RSA private key + * in PEM format. And yes, this is may seem to be a little confusing, the + * method is called "publickey" in the SSH-2 protocol specification, however + * since we need to generate a signature, you actually have to supply a + * private key =). + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + * + * @param user + * A String holding the username. + * @param pair + * A KeyPair containing a RSAPrivateKey, + * DSAPrivateKey, or ECPrivateKey and + * corresponding PublicKey. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, KeyPair pair) + throws IOException + { + if (pair == null) + throw new IllegalArgumentException("Key pair argument is null"); + + checkRequirements(user); + + authenticated = am.authenticatePublicKey(user, pair, getOrCreateSecureRND()); + + return authenticated; + } + /** + * A convenience wrapper function which reads in a private key (PEM format, + * either DSA, EC, or RSA) and then calls + * authenticateWithPublicKey(String, char[], String). + *

+ * NOTE PUTTY USERS: Event though your key file may start with + * "-----BEGIN..." it is not in the expected format. You have to convert it + * to the OpenSSH key format by using the "puttygen" tool (can be downloaded + * from the Putty website). Simply load your key and then use the + * "Conversions/Export OpenSSH key" functionality to get a proper PEM file. + * + * @param user + * A String holding the username. + * @param pemFile + * A File object pointing to a file containing a + * DSA, EC, or RSA private key of the user in OpenSSH key format + * (PEM, you can't miss the "-----BEGIN DSA PRIVATE KEY-----", + * "-----BEGIN EC PRIVATE KEY-----", or + * "-----BEGIN RSA PRIVATE KEY-----" tag). + * @param password + * If the PEM file is encrypted then you must specify the + * password. Otherwise, this argument will be ignored and can be + * set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, File pemFile, String password) + throws IOException + { + if (pemFile == null) + throw new IllegalArgumentException("pemFile argument is null"); + + char[] buff = new char[256]; + + CharArrayWriter cw = new CharArrayWriter(); + + FileReader fr = new FileReader(pemFile); + + while (true) + { + int len = fr.read(buff); + if (len < 0) + break; + cw.write(buff, 0, len); + } + + fr.close(); + + return authenticateWithPublicKey(user, cw.toCharArray(), password); + } + + /** + * After a successful connect, one has to authenticate oneself. The + * authentication method "publickey" works by signing a challenge sent by + * the server. The signature is either DSA, EC, EdDSA or RSA based depending + * on the SignatureProxy specified. The SignatureProxy should sign the + * server's challenge in order to authenticate the user. The user of this library + * is no longer forced to pass the private key. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + * + * @param user + * A String holding the username. + * @param signatureProxy + * A SignatureProxy containing a public key and implementing + * a sign method. + * + * @return Whether the connection is now authenticated. + * @throws IOException Might be thrown if the authentication process threw an error. + */ + public synchronized boolean authenticateWithPublicKey(String user, SignatureProxy signatureProxy) + throws IOException + { + checkRequirements(user); + + if (signatureProxy.getPublicKey() == null) + { + throw new IllegalArgumentException("Signature manager does not contain a public key."); + } + + authenticated = am.authenticatePublicKey(user, signatureProxy); + + return authenticated; + } + + private void checkRequirements(String user) + { + if (tm == null) + { + throw new IllegalStateException("Connection is not established!"); + } + + if (authenticated) + { + throw new IllegalStateException("Connection is already authenticated!"); + } + + if (am == null) + { + am = new AuthenticationManager(tm); + } + + if (cm == null) + { + cm = new ChannelManager(tm); + } + + if (user == null) + { + throw new IllegalArgumentException("user argument is null"); + } + } + + /** + * Add a {@link ConnectionMonitor} to this connection. Can be invoked at any + * time, but it is best to add connection monitors before invoking + * connect() to avoid glitches (e.g., you add a connection + * monitor after a successful connect(), but the connection has died in the + * mean time. Then, your connection monitor won't be notified.) + *

+ * You can add as many monitors as you like. + * + * @see ConnectionMonitor + * + * @param cmon + * An object implementing the ConnectionMonitor + * interface. + */ + public synchronized void addConnectionMonitor(ConnectionMonitor cmon) + { + if (cmon == null) + throw new IllegalArgumentException("cmon argument is null"); + + connectionMonitors.addElement(cmon); + + if (tm != null) + tm.setConnectionMonitors(connectionMonitors); + } + + /** + * Controls whether compression is used on the link or not. + *

+ * Note: This can only be called before connect() + * @param enabled whether to enable compression + * @throws IOException + */ + public synchronized void setCompression(boolean enabled) throws IOException { + if (tm != null) + throw new IOException("Connection to " + hostname + " is already in connected state!"); + + compression = enabled; + } + + /** + * Close the connection to the SSH-2 server. All assigned sessions will be + * closed, too. Can be called at any time. Don't forget to call this once + * you don't need a connection anymore - otherwise the receiver thread may + * run forever. + */ + public synchronized void close() + { + Throwable t = new Throwable("Closed due to user request."); + close(t, false); + } + + private void close(Throwable t, boolean hard) + { + if (cm != null) + cm.closeAllChannels(); + + if (tm != null) + { + tm.close(t, !hard); + tm = null; + } + am = null; + cm = null; + authenticated = false; + } + + /** + * Same as + * {@link #connect(ServerHostKeyVerifier, int, int) connect(null, 0, 0)}. + * + * @return see comments for the + * {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} + * method. + * @throws IOException + */ + public synchronized ConnectionInfo connect() throws IOException + { + return connect(null, 0, 0); + } + + /** + * Same as + * {@link #connect(ServerHostKeyVerifier, int, int) connect(verifier, 0, 0)}. + * + * @return see comments for the + * {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} + * method. + * @throws IOException + */ + public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier) throws IOException + { + return connect(verifier, 0, 0); + } + + /** + * Connect to the SSH-2 server and, as soon as the server has presented its + * host key, use the + * {@link ServerHostKeyVerifier#verifyServerHostKey(String, int, String, + * byte[]) ServerHostKeyVerifier.verifyServerHostKey()} method of the + * verifier to ask for permission to proceed. If + * verifier is null, then any host key will + * be accepted - this is NOT recommended, since it makes man-in-the-middle + * attackes VERY easy (somebody could put a proxy SSH server between you and + * the real server). + *

+ * Note: The verifier will be called before doing any crypto calculations + * (i.e., diffie-hellman). Therefore, if you don't like the presented host + * key then no CPU cycles are wasted (and the evil server has less + * information about us). + *

+ * However, it is still possible that the server presented a fake host key: + * the server cheated (typically a sign for a man-in-the-middle attack) and + * is not able to generate a signature that matches its host key. Don't + * worry, the library will detect such a scenario later when checking the + * signature (the signature cannot be checked before having completed the + * diffie-hellman exchange). + *

+ * Note 2: The {@link ServerHostKeyVerifier#verifyServerHostKey(String, int, + * String, byte[]) ServerHostKeyVerifier.verifyServerHostKey()} method will + * *NOT* be called from the current thread, the call is being made from a + * background thread (there is a background dispatcher thread for every + * established connection). + *

+ * Note 3: This method will block as long as the key exchange of the + * underlying connection has not been completed (and you have not specified + * any timeouts). + *

+ * Note 4: If you want to re-use a connection object that was successfully + * connected, then you must call the {@link #close()} method before invoking + * connect() again. + * + * @param verifier + * An object that implements the {@link ServerHostKeyVerifier} + * interface. Pass null to accept any server host + * key - NOT recommended. + * + * @param connectTimeout + * Connect the underlying TCP socket to the server with the given + * timeout value (non-negative, in milliseconds). Zero means no + * timeout. If a proxy is being used (see + * {@link #setProxyData(ProxyData)}), then this timeout is used + * for the connection establishment to the proxy. + * + * @param kexTimeout + * Timeout for complete connection establishment (non-negative, + * in milliseconds). Zero means no timeout. The timeout counts + * from the moment you invoke the connect() method and is + * cancelled as soon as the first key-exchange round has + * finished. It is possible that the timeout event will be fired + * during the invocation of the verifier callback, + * but it will only have an effect after the + * verifier returns. + * + * @return A {@link ConnectionInfo} object containing the details of the + * established connection. + * + * @throws IOException + * If any problem occurs, e.g., the server's host key is not + * accepted by the verifier or there is problem + * during the initial crypto setup (e.g., the signature sent by + * the server is wrong). + *

+ * In case of a timeout (either connectTimeout or kexTimeout) a + * SocketTimeoutException is thrown. + *

+ * An exception may also be thrown if the connection was already + * successfully connected (no matter if the connection broke in + * the mean time) and you invoke connect() again + * without having called {@link #close()} first. + *

+ * If a HTTP proxy is being used and the proxy refuses the + * connection, then a {@link HTTPProxyException} may be thrown, + * which contains the details returned by the proxy. If the + * proxy is buggy and does not return a proper HTTP response, + * then a normal IOException is thrown instead. + */ + public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier, int connectTimeout, int kexTimeout) + throws IOException + { + final class TimeoutState + { + boolean isCancelled = false; + boolean timeoutSocketClosed = false; + } + + if (tm != null) + throw new IOException("Connection to " + hostname + " is already in connected state!"); + + if (connectTimeout < 0) + throw new IllegalArgumentException("connectTimeout must be non-negative!"); + + if (kexTimeout < 0) + throw new IllegalArgumentException("kexTimeout must be non-negative!"); + + final TimeoutState state = new TimeoutState(); + + tm = new TransportManager(hostname, port); + + tm.setConnectionMonitors(connectionMonitors); + + // Don't offer compression if not requested + if (!compression) { + cryptoWishList.c2s_comp_algos = new String[] { "none" }; + cryptoWishList.s2c_comp_algos = new String[] { "none" }; + } + + /* + * Make sure that the runnable below will observe the new value of "tm" + * and "state" (the runnable will be executed in a different thread, + * which may be already running, that is why we need a memory barrier + * here). See also the comment in Channel.java if you are interested in + * the details. + * + * OKOK, this is paranoid since adding the runnable to the todo list of + * the TimeoutService will ensure that all writes have been flushed + * before the Runnable reads anything (there is a synchronized block in + * TimeoutService.addTimeoutHandler). + */ + + synchronized (tm) + { + /* We could actually synchronize on anything. */ + } + + try + { + TimeoutToken token = null; + + if (kexTimeout > 0) + { + final Runnable timeoutHandler = new Runnable() + { + public void run() + { + synchronized (state) + { + if (state.isCancelled) + return; + state.timeoutSocketClosed = true; + tm.close(new SocketTimeoutException("The connect timeout expired"), false); + } + } + }; + + long timeoutHorizont = System.currentTimeMillis() + kexTimeout; + + token = TimeoutService.addTimeoutHandler(timeoutHorizont, timeoutHandler); + } + + try + { + tm.initialize(cryptoWishList, verifier, dhgexpara, connectTimeout, getOrCreateSecureRND(), proxyData); + } + catch (SocketTimeoutException se) + { + throw (SocketTimeoutException) new SocketTimeoutException( + "The connect() operation on the socket timed out.").initCause(se); + } + + /* Wait until first KEX has finished */ + + ConnectionInfo ci = tm.getConnectionInfo(1); + + /* Now try to cancel the timeout, if needed */ + + if (token != null) + { + TimeoutService.cancelTimeoutHandler(token); + + /* Were we too late? */ + + synchronized (state) + { + if (state.timeoutSocketClosed) + throw new IOException("This exception will be replaced by the one below =)"); + /* + * Just in case the "cancelTimeoutHandler" invocation came + * just a little bit too late but the handler did not enter + * the semaphore yet - we can still stop it. + */ + state.isCancelled = true; + } + } + + return ci; + } + catch (SocketTimeoutException ste) + { + throw ste; + } + catch (IOException e1) + { + /* This will also invoke any registered connection monitors */ + close(new Throwable("There was a problem during connect."), false); + + synchronized (state) + { + /* + * Show a clean exception, not something like "the socket is + * closed!?!" + */ + if (state.timeoutSocketClosed) + throw new SocketTimeoutException("The kexTimeout (" + kexTimeout + " ms) expired."); + } + + /* Do not wrap a HTTPProxyException */ + if (e1 instanceof HTTPProxyException) + throw e1; + + throw new IOException("There was a problem while connecting to " + hostname + ":" + port, e1); + } + } + + /** + * Creates a new {@link LocalPortForwarder}. A + * LocalPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host (which may or may + * not be identical to the remote SSH-2 server). + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param local_port + * the local port the LocalPortForwarder shall bind to. + * @param host_to_connect + * target address (IP or hostname) + * @param port_to_connect + * target port + * @return A {@link LocalPortForwarder} object. + * @throws IOException + */ + public synchronized LocalPortForwarder createLocalPortForwarder(int local_port, String host_to_connect, + int port_to_connect) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new LocalPortForwarder(cm, local_port, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link LocalPortForwarder}. A + * LocalPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host (which may or may + * not be identical to the remote SSH-2 server). + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param addr + * specifies the InetSocketAddress where the local socket shall + * be bound to. + * @param host_to_connect + * target address (IP or hostname) + * @param port_to_connect + * target port + * @return A {@link LocalPortForwarder} object. + * @throws IOException + */ + public synchronized LocalPortForwarder createLocalPortForwarder(InetSocketAddress addr, String host_to_connect, + int port_to_connect) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new LocalPortForwarder(cm, addr, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link LocalStreamForwarder}. A + * LocalStreamForwarder manages an Input/Outputstream pair + * that is being forwarded via the secure tunnel into a TCP/IP connection to + * another host (which may or may not be identical to the remote SSH-2 + * server). + * + * @param host_to_connect + * @param port_to_connect + * @return A {@link LocalStreamForwarder} object. + * @throws IOException + */ + public synchronized LocalStreamForwarder createLocalStreamForwarder(String host_to_connect, int port_to_connect) + throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward, connection is not authenticated."); + + return new LocalStreamForwarder(cm, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link DynamicPortForwarder}. A + * DynamicPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host that is chosen via + * the SOCKS protocol. + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param local_port + * @return A {@link DynamicPortForwarder} object. + * @throws IOException + */ + public synchronized DynamicPortForwarder createDynamicPortForwarder(int local_port) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new DynamicPortForwarder(cm, local_port); + } + + /** + * Creates a new {@link DynamicPortForwarder}. A + * DynamicPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host that is chosen via + * the SOCKS protocol. + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param addr + * specifies the InetSocketAddress where the local socket shall + * be bound to. + * @return A {@link DynamicPortForwarder} object. + * @throws IOException + */ + public synchronized DynamicPortForwarder createDynamicPortForwarder(InetSocketAddress addr) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new DynamicPortForwarder(cm, addr); + } + + /** + * Create a very basic {@link SCPClient} that can be used to copy files + * from/to the SSH-2 server. + *

+ * Works only after one has passed successfully the authentication step. + * There is no limit on the number of concurrent SCP clients. + *

+ * Note: This factory method will probably disappear in the future. + * + * @return A {@link SCPClient} object. + */ + public synchronized SCPClient createSCPClient() { + if (tm == null) + throw new IllegalStateException("Cannot create SCP client, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot create SCP client, connection is not authenticated."); + + return new SCPClient(this); + } + + /** + * Force an asynchronous key re-exchange (the call does not block). The + * latest values set for MAC, Cipher and DH group exchange parameters will + * be used. If a key exchange is currently in progress, then this method has + * the only effect that the so far specified parameters will be used for the + * next (server driven) key exchange. + *

+ * Note: This implementation will never start a key exchange (other than the + * initial one) unless you or the SSH-2 server ask for it. + * + * @throws IOException + * In case of any failure behind the scenes. + */ + public synchronized void forceKeyExchange() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + tm.forceKeyExchange(cryptoWishList, dhgexpara); + } + + /** + * Returns the hostname that was passed to the constructor. + * + * @return the hostname + */ + public synchronized String getHostname() + { + return hostname; + } + + /** + * Returns the port that was passed to the constructor. + * + * @return the TCP port + */ + public synchronized int getPort() + { + return port; + } + + /** + * Returns a {@link ConnectionInfo} object containing the details of the + * connection. Can be called as soon as the connection has been established + * (successfully connected). + * + * @return A {@link ConnectionInfo} object. + * @throws IOException + * In case of any failure behind the scenes. + */ + public synchronized ConnectionInfo getConnectionInfo() throws IOException + { + if (tm == null) + throw new IllegalStateException( + "Cannot get details of connection, you need to establish a connection first."); + return tm.getConnectionInfo(1); + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * can be used to tell which authentication methods are supported by the + * server at a certain stage of the authentication process (for the given + * username). + *

+ * Note 1: the username will only be used if no authentication step was done + * so far (it will be used to ask the server for a list of possible + * authentication methods by sending the initial "none" request). Otherwise, + * this method ignores the user name and returns a cached method list (which + * is based on the information contained in the last negative server + * response). + *

+ * Note 2: the server may return method names that are not supported by this + * implementation. + *

+ * After a successful authentication, this method must not be called + * anymore. + * + * @param user + * A String holding the username. + * + * @return a (possibly emtpy) array holding authentication method names. + * @throws IOException + */ + public synchronized String[] getRemainingAuthMethods(String user) throws IOException + { + if (user == null) + throw new IllegalArgumentException("user argument may not be NULL!"); + + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + return am.getRemainingMethods(user); + } + + /** + * Determines if the authentication phase is complete. Can be called at any + * time. + * + * @return true if no further authentication steps are + * needed. + */ + public synchronized boolean isAuthenticationComplete() + { + return authenticated; + } + + /** + * Returns true if there was at least one failed authentication request and + * the last failed authentication request was marked with "partial success" + * by the server. This is only needed in the rare case of SSH-2 server + * setups that cannot be satisfied with a single successful authentication + * request (i.e., multiple authentication steps are needed.) + *

+ * If you are interested in the details, then have a look at RFC4252. + * + * @return if the there was a failed authentication step and the last one + * was marked as a "partial success". + */ + public synchronized boolean isAuthenticationPartialSuccess() + { + if (am == null) + return false; + + return am.getPartialSuccess(); + } + + /** + * Checks if a specified authentication method is available. This method is + * actually just a wrapper for {@link #getRemainingAuthMethods(String) + * getRemainingAuthMethods()}. + * + * @param user + * A String holding the username. + * @param method + * An authentication method name (e.g., "publickey", "password", + * "keyboard-interactive") as specified by the SSH-2 standard. + * @return if the specified authentication method is currently available. + * @throws IOException + */ + public synchronized boolean isAuthMethodAvailable(String user, String method) throws IOException + { + if (method == null) + throw new IllegalArgumentException("method argument may not be NULL!"); + + String methods[] = getRemainingAuthMethods(user); + + for (int i = 0; i < methods.length; i++) + { + if (methods[i].compareTo(method) == 0) + return true; + } + + return false; + } + + private final SecureRandom getOrCreateSecureRND() + { + if (generator == null) + generator = new SecureRandom(); + + return generator; + } + + /** + * Open a new {@link Session} on this connection. Works only after one has + * passed successfully the authentication step. There is no limit on the + * number of concurrent sessions. + * + * @return A {@link Session} object. + * @throws IOException + */ + public synchronized Session openSession() throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot open session, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot open session, connection is not authenticated."); + + return new Session(cm, getOrCreateSecureRND()); + } + + /** + * Send an SSH_MSG_IGNORE packet. This method will generate a random data + * attribute (length between 0 (invlusive) and 16 (exclusive) bytes, + * contents are random bytes). + *

+ * This method must only be called once the connection is established. + * + * @throws IOException + */ + public synchronized void sendIgnorePacket() throws IOException + { + SecureRandom rnd = getOrCreateSecureRND(); + + byte[] data = new byte[rnd.nextInt(16)]; + rnd.nextBytes(data); + + sendIgnorePacket(data); + } + + /** + * Send an SSH_MSG_IGNORE packet with the given data attribute. + *

+ * This method must only be called once the connection is established. + * + * @throws IOException + */ + public synchronized void sendIgnorePacket(byte[] data) throws IOException + { + if (data == null) + throw new IllegalArgumentException("data argument must not be null."); + + if (tm == null) + throw new IllegalStateException( + "Cannot send SSH_MSG_IGNORE packet, you need to establish a connection first."); + + PacketIgnore pi = new PacketIgnore(); + pi.setData(data); + + tm.sendMessage(pi.getPayload()); + } + + /** + * Removes duplicates from a String array, keeps only first occurence of + * each element. Does not destroy order of elements; can handle nulls. Uses + * a very efficient O(N^2) algorithm =) + * + * @param list + * a String array. + * @return a cleaned String array. + */ + private String[] removeDuplicates(String[] list) + { + if ((list == null) || (list.length < 2)) + return list; + + String[] list2 = new String[list.length]; + + int count = 0; + + for (int i = 0; i < list.length; i++) + { + boolean duplicate = false; + + String element = list[i]; + + for (int j = 0; j < count; j++) + { + if (element == null ? list2[j] == null : element.equals(list2[j])) + { + duplicate = true; + break; + } + } + + if (duplicate) + continue; + + list2[count++] = list[i]; + } + + if (count == list2.length) + return list2; + + String[] tmp = new String[count]; + System.arraycopy(list2, 0, tmp, 0, count); + + return tmp; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param ciphers + */ + public synchronized void setClient2ServerCiphers(String[] ciphers) + { + if ((ciphers == null) || (ciphers.length == 0)) + throw new IllegalArgumentException(); + ciphers = removeDuplicates(ciphers); + BlockCipherFactory.checkCipherList(ciphers); + cryptoWishList.c2s_enc_algos = ciphers; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param macs + */ + public synchronized void setClient2ServerMACs(String[] macs) + { + if ((macs == null) || (macs.length == 0)) + throw new IllegalArgumentException(); + macs = removeDuplicates(macs); + MACs.checkMacList(macs); + cryptoWishList.c2s_mac_algos = macs; + } + + /** + * Sets the parameters for the diffie-hellman group exchange. Unless you + * know what you are doing, you will never need this. Default values are + * defined in the {@link DHGexParameters} class. + * + * @param dgp + * {@link DHGexParameters}, non null. + * + */ + public synchronized void setDHGexParameters(DHGexParameters dgp) + { + if (dgp == null) + throw new IllegalArgumentException(); + + dhgexpara = dgp; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param ciphers + */ + public synchronized void setServer2ClientCiphers(String[] ciphers) + { + if ((ciphers == null) || (ciphers.length == 0)) + throw new IllegalArgumentException(); + ciphers = removeDuplicates(ciphers); + BlockCipherFactory.checkCipherList(ciphers); + cryptoWishList.s2c_enc_algos = ciphers; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param macs + */ + public synchronized void setServer2ClientMACs(String[] macs) + { + if ((macs == null) || (macs.length == 0)) + throw new IllegalArgumentException(); + + macs = removeDuplicates(macs); + MACs.checkMacList(macs); + cryptoWishList.s2c_mac_algos = macs; + } + + /** + * Define the set of allowed server host key algorithms to be used for the + * following key exchange operations. + *

+ * Unless you know what you are doing, you will never need this. + * + * @param algos + * An array of allowed server host key algorithms. SSH-2 defines + * ssh-dss and ssh-rsa. The + * entries of the array must be ordered after preference, i.e., + * the entry at index 0 is the most preferred one. You must + * specify at least one entry. + */ + public synchronized void setServerHostKeyAlgorithms(String[] algos) + { + if ((algos == null) || (algos.length == 0)) + throw new IllegalArgumentException(); + + algos = removeDuplicates(algos); + KexManager.checkServerHostkeyAlgorithmsList(algos); + cryptoWishList.serverHostKeyAlgorithms = algos; + } + + /** + * Define the set of allowed Key Exchange algorithms to be used for the + * following key exchange operations. + *

+ * Unless you know what you are doing, you will never need this. + * + * @param algos + * An array of allowed key exchange algorithms. + */ + public synchronized void setKeyExchangeAlgorithms(String[] algos) + { + if (algos == null || algos.length == 0) + throw new IllegalArgumentException(); + + algos = removeDuplicates(algos); + KexManager.checkKexAlgorithmList(algos); + cryptoWishList.kexAlgorithms = algos; + } + + /** + * Used to tell the library that the connection shall be established through + * a proxy server. It only makes sense to call this method before calling + * the {@link #connect() connect()} method. + *

+ * At the moment, only HTTP proxies are supported. + *

+ * Note: This method can be called any number of times. The + * {@link #connect() connect()} method will use the value set in the last + * preceding invocation of this method. + * + * @see HTTPProxyData + * + * @param proxyData + * Connection information about the proxy. If null, + * then no proxy will be used (non surprisingly, this is also the + * default). + */ + public synchronized void setProxyData(ProxyData proxyData) + { + this.proxyData = proxyData; + } + + /** + * Request a remote port forwarding. If successful, then forwarded + * connections will be redirected to the given target address. You can + * cancle a requested remote port forwarding by calling + * {@link #cancelRemotePortForwarding(int) cancelRemotePortForwarding()}. + *

+ * A call of this method will block until the peer either agreed or + * disagreed to your request- + *

+ * Note 1: this method typically fails if you + *

    + *
  • pass a port number for which the used remote user has not enough + * permissions (i.e., port < 1024)
  • + *
  • or pass a port number that is already in use on the remote server
  • + *
  • or if remote port forwarding is disabled on the server.
  • + *
+ *

+ * Note 2: (from the openssh man page): By default, the listening socket on + * the server will be bound to the loopback interface only. This may be + * overriden by specifying a bind address. Specifying a remote bind address + * will only succeed if the server's GatewayPorts option is enabled + * (see sshd_config(5)). + * + * @param bindAddress + * address to bind to on the server: + *

    + *
  • "" means that connections are to be accepted on all + * protocol families supported by the SSH implementation
  • + *
  • "0.0.0.0" means to listen on all IPv4 addresses
  • + *
  • "::" means to listen on all IPv6 addresses
  • + *
  • "localhost" means to listen on all protocol families + * supported by the SSH implementation on loopback addresses + * only, [RFC3330] and RFC3513]
  • + *
  • "127.0.0.1" and "::1" indicate listening on the loopback + * interfaces for IPv4 and IPv6 respectively
  • + *
+ * @param bindPort + * port number to bind on the server (must be > 0) + * @param targetAddress + * the target address (IP or hostname) + * @param targetPort + * the target port + * @throws IOException + */ + public synchronized void requestRemotePortForwarding(String bindAddress, int bindPort, String targetAddress, + int targetPort) throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + if ((bindAddress == null) || (targetAddress == null) || (bindPort <= 0) || (targetPort <= 0)) + throw new IllegalArgumentException(); + + cm.requestGlobalForward(bindAddress, bindPort, targetAddress, targetPort); + } + + /** + * Cancel an earlier requested remote port forwarding. Currently active + * forwardings will not be affected (e.g., disrupted). Note that further + * connection forwarding requests may be received until this method has + * returned. + * + * @param bindPort + * the allocated port number on the server + * @throws IOException + * if the remote side refuses the cancel request or another low + * level error occurs (e.g., the underlying connection is + * closed) + */ + public synchronized void cancelRemotePortForwarding(int bindPort) throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + cm.requestCancelGlobalForward(bindPort); + } + + /** + * Provide your own instance of SecureRandom. Can be used, e.g., if you want + * to seed the used SecureRandom generator manually. + *

+ * The SecureRandom instance is used during key exchanges, public key + * authentication, x11 cookie generation and the like. + * + * @param rnd + * a SecureRandom instance + */ + public synchronized void setSecureRandom(SecureRandom rnd) + { + if (rnd == null) + throw new IllegalArgumentException(); + + this.generator = rnd; + } + + /** + * Enable/disable debug logging. Only do this when requested by Trilead + * support. + *

+ * For speed reasons, some static variables used to check whether debugging + * is enabled are not protected with locks. In other words, if you + * dynamicaly enable/disable debug logging, then some threads may still use + * the old setting. To be on the safe side, enable debugging before doing + * the connect() call. + * + * @param enable + * on/off + * @param logger + * a {@link DebugLogger DebugLogger} instance, null + * means logging using the simple logger which logs all messages + * to to stderr. Ignored if enabled is false + */ + public synchronized void enableDebugging(boolean enable, DebugLogger logger) + { + Logger.enabled = enable; + + if (!enable) + { + Logger.logger = null; + } + else + { + if (logger == null) + { + logger = new DebugLogger() + { + + public void log(int level, String className, String message) + { + long now = System.currentTimeMillis(); + System.err.println(now + " : " + className + ": " + message); + } + }; + } + + Logger.logger = logger; + } + } + + /** + * This method can be used to perform end-to-end connection testing. It + * sends a 'ping' message to the server and waits for the 'pong' from the + * server. + *

+ * When this method throws an exception, then you can assume that the + * connection should be abandoned. + *

+ * Note: Works only after one has passed successfully the authentication + * step. + *

+ * Implementation details: this method sends a SSH_MSG_GLOBAL_REQUEST + * request ('trilead-ping') to the server and waits for the + * SSH_MSG_REQUEST_FAILURE reply packet from the server. + * + * @throws IOException + * in case of any problem + */ + public synchronized void ping() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + cm.requestGlobalTrileadPing(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionInfo.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionInfo.java new file mode 100644 index 0000000000..d508acf514 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionInfo.java @@ -0,0 +1,65 @@ + +package com.trilead.ssh2; + +/** + * In most cases you probably do not need the information contained in here. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ConnectionInfo.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class ConnectionInfo +{ + /** + * The used key exchange (KEX) algorithm in the latest key exchange. + */ + public String keyExchangeAlgorithm; + + /** + * The currently used crypto algorithm for packets from to the client to the + * server. + */ + public String clientToServerCryptoAlgorithm; + /** + * The currently used crypto algorithm for packets from to the server to the + * client. + */ + public String serverToClientCryptoAlgorithm; + + /** + * The currently used MAC algorithm for packets from to the client to the + * server. + */ + public String clientToServerMACAlgorithm; + /** + * The currently used MAC algorithm for packets from to the server to the + * client. + */ + public String serverToClientMACAlgorithm; + + /** + * The type of the server host key (currently either "ssh-dss" or + * "ssh-rsa"). + */ + public String serverHostKeyAlgorithm; + /** + * The server host key that was sent during the latest key exchange. + */ + public byte[] serverHostKey; + + /** + * Number of kex exchanges performed on this connection so far. + */ + public int keyExchangeCounter = 0; + + /** + * The currently used compression algorithm for packets from the client to + * the server. + */ + public String clientToServerCompressionAlgorithm; + + /** + * The currently used compression algorithm for packets from the server to + * the client. + */ + public String serverToClientCompressionAlgorithm; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionMonitor.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionMonitor.java new file mode 100644 index 0000000000..59a9766d25 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ConnectionMonitor.java @@ -0,0 +1,34 @@ + +package com.trilead.ssh2; + +/** + * A ConnectionMonitor is used to get notified when the + * underlying socket of a connection is closed. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ConnectionMonitor.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface ConnectionMonitor +{ + /** + * This method is called after the connection's underlying + * socket has been closed. E.g., due to the {@link Connection#close()} request of the + * user, if the peer closed the connection, due to a fatal error during connect() + * (also if the socket cannot be established) or if a fatal error occured on + * an established connection. + *

+ * This is an experimental feature. + *

+ * You MUST NOT make any assumption about the thread that invokes this method. + *

+ * Please note: if the connection is not connected (e.g., there was no successful + * connect() call), then the invocation of {@link Connection#close()} will NOT trigger + * this method. + * + * @see Connection#addConnectionMonitor(ConnectionMonitor) + * + * @param reason Includes an indication why the socket was closed. + */ + void connectionLost(Throwable reason); +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DHGexParameters.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DHGexParameters.java new file mode 100644 index 0000000000..e779600e20 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DHGexParameters.java @@ -0,0 +1,121 @@ + +package com.trilead.ssh2; + +/** + * A DHGexParameters object can be used to specify parameters for + * the diffie-hellman group exchange. + *

+ * Depending on which constructor is used, either the use of a + * SSH_MSG_KEX_DH_GEX_REQUEST or SSH_MSG_KEX_DH_GEX_REQUEST_OLD + * can be forced. + * + * @see Connection#setDHGexParameters(DHGexParameters) + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DHGexParameters.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class DHGexParameters +{ + private final int min_group_len; + private final int pref_group_len; + private final int max_group_len; + + private static final int MIN_ALLOWED = 1024; + private static final int MAX_ALLOWED = 8192; + + /** + * Same as calling {@link #DHGexParameters(int, int, int) DHGexParameters(1024, 1024, 4096)}. + * This is also the default used by the Connection class. + * + */ + public DHGexParameters() + { + this(1024, 1024, 4096); + } + + /** + * This constructor can be used to force the sending of a + * SSH_MSG_KEX_DH_GEX_REQUEST_OLD request. + * Internally, the minimum and maximum group lengths will + * be set to zero. + * + * @param pref_group_len has to be >= 1024 and <= 8192 + */ + public DHGexParameters(int pref_group_len) + { + if ((pref_group_len < MIN_ALLOWED) || (pref_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("pref_group_len out of range!"); + + this.pref_group_len = pref_group_len; + this.min_group_len = 0; + this.max_group_len = 0; + } + + /** + * This constructor can be used to force the sending of a + * SSH_MSG_KEX_DH_GEX_REQUEST request. + *

+ * Note: older OpenSSH servers don't understand this request, in which + * case you should use the {@link #DHGexParameters(int)} constructor. + *

+ * All values have to be >= 1024 and <= 8192. Furthermore, + * min_group_len <= pref_group_len <= max_group_len. + * + * @param min_group_len + * @param pref_group_len + * @param max_group_len + */ + public DHGexParameters(int min_group_len, int pref_group_len, int max_group_len) + { + if ((min_group_len < MIN_ALLOWED) || (min_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("min_group_len out of range!"); + + if ((pref_group_len < MIN_ALLOWED) || (pref_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("pref_group_len out of range!"); + + if ((max_group_len < MIN_ALLOWED) || (max_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("max_group_len out of range!"); + + if ((pref_group_len < min_group_len) || (pref_group_len > max_group_len)) + throw new IllegalArgumentException("pref_group_len is incompatible with min and max!"); + + if (max_group_len < min_group_len) + throw new IllegalArgumentException("max_group_len must not be smaller than min_group_len!"); + + this.min_group_len = min_group_len; + this.pref_group_len = pref_group_len; + this.max_group_len = max_group_len; + } + + /** + * Get the maximum group length. + * + * @return the maximum group length, may be zero if + * SSH_MSG_KEX_DH_GEX_REQUEST_OLD should be requested + */ + public int getMax_group_len() + { + return max_group_len; + } + + /** + * Get the minimum group length. + * + * @return minimum group length, may be zero if + * SSH_MSG_KEX_DH_GEX_REQUEST_OLD should be requested + */ + public int getMin_group_len() + { + return min_group_len; + } + + /** + * Get the preferred group length. + * + * @return the preferred group length + */ + public int getPref_group_len() + { + return pref_group_len; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DebugLogger.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DebugLogger.java new file mode 100644 index 0000000000..f1ede720d0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DebugLogger.java @@ -0,0 +1,23 @@ +package com.trilead.ssh2; + +/** + * An interface which needs to be implemented if you + * want to capture debugging messages. + * + * @see Connection#enableDebugging(boolean, DebugLogger) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DebugLogger.java,v 1.1 2008/03/03 07:01:36 cplattne Exp $ + */ +public interface DebugLogger +{ + +/** + * Log a debug message. + * + * @param level 0-99, 99 is a the most verbose level + * @param className the class that generated the message + * @param message the debug message + */ +void log(int level, String className, String message); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java new file mode 100644 index 0000000000..db3a0682c9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.DynamicAcceptThread; + +/** + * A DynamicPortForwarder forwards TCP/IP connections to a local + * port via the secure tunnel to another host which is selected via the + * SOCKS protocol. Checkout {@link Connection#createDynamicPortForwarder(int)} + * on how to create one. + * + * @author Kenny Root + * @version $Id: $ + */ +public class DynamicPortForwarder { + ChannelManager cm; + + DynamicAcceptThread dat; + + DynamicPortForwarder(ChannelManager cm, int local_port) + throws IOException + { + this.cm = cm; + + dat = new DynamicAcceptThread(cm, local_port); + dat.setDaemon(true); + dat.start(); + } + + DynamicPortForwarder(ChannelManager cm, InetSocketAddress addr) throws IOException { + this.cm = cm; + + dat = new DynamicAcceptThread(cm, addr); + dat.setDaemon(true); + dat.start(); + } + + /** + * Stop TCP/IP forwarding of newly arriving connections. + * + */ + public void close() { + dat.stopWorking(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtendedServerHostKeyVerifier.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtendedServerHostKeyVerifier.java new file mode 100644 index 0000000000..f757aa6b97 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtendedServerHostKeyVerifier.java @@ -0,0 +1,47 @@ +package com.trilead.ssh2; + +import java.util.List; + +/** + * This extends the {@link ServerHostKeyVerifier} interface by allowing the remote server to indicate it has multiple + * server key algorithms available. After authentication, the {@link #getKnownKeyAlgorithmsForHost(String, int)} method + * may be called and compared against the list of server-controller keys. If a key algorithm has been added then + * {@link #addServerHostKey(String, int, String, byte[])} will be called. If a key algorithm has been removed, then + * {@link #removeServerHostKey(String, int, String, byte[])} will be called. + * + * @author Kenny Root + */ +public abstract class ExtendedServerHostKeyVerifier implements ServerHostKeyVerifier { + /** + * Called during connection to determine which keys are known for this host. + * + * @param hostname the hostname used to create the {@link Connection} object + * @param port the server's remote TCP port + * @return list of hostkey algorithms for the given hostname and port combination + * or {@code null} if none are known. + */ + public abstract List getKnownKeyAlgorithmsForHost(String hostname, int port); + + /** + * After authentication, if the server indicates it no longer uses this key, this method will be called + * for the app to remove its record of it. + * + * @param hostname the hostname used to create the {@link Connection} object + * @param port the server's remote TCP port + * @param serverHostKeyAlgorithm key algorithm of removed key + * @param serverHostKey key data of removed key + */ + public abstract void removeServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, + byte[] serverHostKey); + + /** + * After authentication, if the server indicates it has another keyAlgorithm, this method will be + * called for the app to add it to its record of known keys for this hostname. + * + * @param hostname the hostname used to create the {@link Connection} object + * @param port the server's remote TCP port + * @param keyAlgorithm SSH standard name for the key to be added + * @param serverHostKey SSH encoding of the key data for the key to be added + */ + public abstract void addServerHostKey(String hostname, int port, String keyAlgorithm, byte[] serverHostKey); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtensionInfo.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtensionInfo.java new file mode 100644 index 0000000000..58e602765d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ExtensionInfo.java @@ -0,0 +1,47 @@ +package com.trilead.ssh2; + +import com.trilead.ssh2.packets.PacketExtInfo; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * SSH extensions reported by the server + * + * https://tools.ietf.org/html/draft-ietf-curdle-ssh-ext-info-15 + */ +public class ExtensionInfo +{ + private final Set signatureAlgorithmsAccepted; + + /** + * @return Signature algorithms that server will accept. If empty, this extension was absent. + */ + public Set getSignatureAlgorithmsAccepted() + { + return signatureAlgorithmsAccepted; + } + + public static ExtensionInfo fromPacketExtInfo(PacketExtInfo packetExtInfo) + { + String rawAlgs = packetExtInfo.getExtNameToValue().get("server-sig-algs"); + if (rawAlgs == null) + { + return new ExtensionInfo(Collections.emptySet()); + } + + Set algsSet = new HashSet<>(); + Collections.addAll(algsSet, rawAlgs.split(",")); + return new ExtensionInfo(algsSet); + } + + public static ExtensionInfo noExtInfoSeen() + { + return new ExtensionInfo(Collections.emptySet()); + } + + private ExtensionInfo(Set signatureAlgorithmsAccepted) + { + this.signatureAlgorithmsAccepted = Collections.unmodifiableSet(signatureAlgorithmsAccepted); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyData.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyData.java new file mode 100644 index 0000000000..3be55a4160 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyData.java @@ -0,0 +1,202 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; + +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.transport.ClientServerHello; + +/** + * A HTTPProxyData object is used to specify the needed connection data + * to connect through a HTTP proxy. + * + * @see Connection#setProxyData(ProxyData) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: HTTPProxyData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class HTTPProxyData implements ProxyData +{ + private final String proxyHost; + private final int proxyPort; + private final String proxyUser; + private final String proxyPass; + private final String[] requestHeaderLines; + + /** + * Same as calling {@link #HTTPProxyData(String, int, String, String) HTTPProxyData(proxyHost, proxyPort, null, null)} + * + * @param proxyHost Proxy hostname. + * @param proxyPort Proxy port. + */ + public HTTPProxyData(String proxyHost, int proxyPort) + { + this(proxyHost, proxyPort, null, null); + } + + /** + * Same as calling {@link #HTTPProxyData(String, int, String, String, String[]) HTTPProxyData(proxyHost, proxyPort, null, null, null)} + * + * @param proxyHost Proxy hostname. + * @param proxyPort Proxy port. + * @param proxyUser Username for basic authentication (null if no authentication is needed). + * @param proxyPass Password for basic authentication (null if no authentication is needed). + */ + public HTTPProxyData(String proxyHost, int proxyPort, String proxyUser, String proxyPass) + { + this(proxyHost, proxyPort, proxyUser, proxyPass, null); + } + + /** + * Connection data for a HTTP proxy. It is possible to specify a username and password + * if the proxy requires basic authentication. Also, additional request header lines can + * be specified (e.g., "User-Agent: CERN-LineMode/2.15 libwww/2.17b3"). + *

+ * Please note: if you want to use basic authentication, then both proxyUser + * and proxyPass must be non-null. + *

+ * Here is an example: + *

+ * + * new HTTPProxyData("192.168.1.1", "3128", "proxyuser", "secret", new String[] {"User-Agent: TrileadBasedClient/1.0", "X-My-Proxy-Option: something"}); + * + * + * @param proxyHost Proxy hostname. + * @param proxyPort Proxy port. + * @param proxyUser Username for basic authentication (null if no authentication is needed). + * @param proxyPass Password for basic authentication (null if no authentication is needed). + * @param requestHeaderLines An array with additional request header lines (without end-of-line markers) + * that have to be sent to the server. May be null. + */ + + public HTTPProxyData(String proxyHost, int proxyPort, String proxyUser, String proxyPass, + String[] requestHeaderLines) + { + if (proxyHost == null) + throw new IllegalArgumentException("proxyHost must be non-null"); + + if (proxyPort < 0) + throw new IllegalArgumentException("proxyPort must be non-negative"); + + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyUser = proxyUser; + this.proxyPass = proxyPass; + this.requestHeaderLines = requestHeaderLines; + } + + @Override + public Socket openConnection(String hostname, int port, int connectTimeout) throws IOException { + Socket sock = new Socket(); + + InetAddress addr = InetAddress.getByName(proxyHost); + sock.connect(new InetSocketAddress(addr, proxyPort), connectTimeout); + sock.setSoTimeout(0); + + /* OK, now tell the proxy where we actually want to connect to */ + + StringBuffer sb = new StringBuffer(); + + sb.append("CONNECT "); + sb.append(hostname); + sb.append(':'); + sb.append(port); + sb.append(" HTTP/1.0\r\n"); + + if ((proxyUser != null) && (proxyPass != null)) + { + String credentials = proxyUser + ":" + proxyPass; + char[] encoded; + try { + encoded = Base64.encode(credentials.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + encoded = Base64.encode(credentials.getBytes()); + } + sb.append("Proxy-Authorization: Basic "); + sb.append(encoded); + sb.append("\r\n"); + } + + if (requestHeaderLines != null) + { + for (int i = 0; i < requestHeaderLines.length; i++) + { + if (requestHeaderLines[i] != null) + { + sb.append(requestHeaderLines[i]); + sb.append("\r\n"); + } + } + } + + sb.append("\r\n"); + + OutputStream out = sock.getOutputStream(); + + try { + out.write(sb.toString().getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + out.write(sb.toString().getBytes()); + } + out.flush(); + + /* Now parse the HTTP response */ + + byte[] buffer = new byte[1024]; + InputStream in = sock.getInputStream(); + + int len = ClientServerHello.readLineRN(in, buffer); + + String httpReponse; + try { + httpReponse = new String(buffer, 0, len, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + httpReponse = new String(buffer, 0, len); + } + + if (!httpReponse.startsWith("HTTP/")) + throw new IOException("The proxy did not send back a valid HTTP response."); + + /* "HTTP/1.X XYZ X" => 14 characters minimum */ + + if ((httpReponse.length() < 14) || (httpReponse.charAt(8) != ' ') || (httpReponse.charAt(12) != ' ')) + throw new IOException("The proxy did not send back a valid HTTP response."); + + int errorCode = 0; + + try + { + errorCode = Integer.parseInt(httpReponse.substring(9, 12)); + } + catch (NumberFormatException ignore) + { + throw new IOException("The proxy did not send back a valid HTTP response."); + } + + if ((errorCode < 0) || (errorCode > 999)) + throw new IOException("The proxy did not send back a valid HTTP response."); + + if (errorCode != 200) + { + throw new HTTPProxyException(httpReponse.substring(13), errorCode); + } + + /* OK, read until empty line */ + + while (true) + { + len = ClientServerHello.readLineRN(in, buffer); + if (len == 0) + break; + } + + return sock; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyException.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyException.java new file mode 100644 index 0000000000..2d2d019b52 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/HTTPProxyException.java @@ -0,0 +1,29 @@ + +package com.trilead.ssh2; + +import java.io.IOException; + +/** + * May be thrown upon connect() if a HTTP proxy is being used. + * + * @see Connection#connect() + * @see Connection#setProxyData(ProxyData) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: HTTPProxyException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class HTTPProxyException extends IOException +{ + private static final long serialVersionUID = 2241537397104426186L; + + public final String httpResponse; + public final int httpErrorCode; + + public HTTPProxyException(String httpResponse, int httpErrorCode) + { + super("HTTP Proxy Error (" + httpErrorCode + " " + httpResponse + ")"); + this.httpResponse = httpResponse; + this.httpErrorCode = httpErrorCode; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/InteractiveCallback.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/InteractiveCallback.java new file mode 100644 index 0000000000..06186efc7b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/InteractiveCallback.java @@ -0,0 +1,55 @@ + +package com.trilead.ssh2; + +/** + * An InteractiveCallback is used to respond to challenges sent + * by the server if authentication mode "keyboard-interactive" is selected. + * + * @see Connection#authenticateWithKeyboardInteractive(String, + * String[], InteractiveCallback) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: InteractiveCallback.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface InteractiveCallback +{ + /** + * This callback interface is used during a "keyboard-interactive" + * authentication. Every time the server sends a set of challenges (however, + * most often just one challenge at a time), this callback function will be + * called to give your application a chance to talk to the user and to + * determine the response(s). + *

+ * Some copy-paste information from the standard: a command line interface + * (CLI) client SHOULD print the name and instruction (if non-empty), adding + * newlines. Then for each prompt in turn, the client SHOULD display the + * prompt and read the user input. The name and instruction fields MAY be + * empty strings, the client MUST be prepared to handle this correctly. The + * prompt field(s) MUST NOT be empty strings. + *

+ * Please refer to draft-ietf-secsh-auth-kbdinteract-XX.txt for the details. + *

+ * Note: clients SHOULD use control character filtering as discussed in + * RFC4251 to avoid attacks by including + * terminal control characters in the fields to be displayed. + * + * @param name + * the name String sent by the server. + * @param instruction + * the instruction String sent by the server. + * @param numPrompts + * number of prompts - may be zero (in this case, you should just + * return a String array of length zero). + * @param prompt + * an array (length numPrompts) of Strings + * @param echo + * an array (length numPrompts) of booleans. For + * each prompt, the corresponding echo field indicates whether or + * not the user input should be echoed as characters are typed. + * @return an array of reponses - the array size must match the parameter + * numPrompts. + */ + String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) + throws Exception; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/KnownHosts.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/KnownHosts.java new file mode 100644 index 0000000000..030ad70371 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/KnownHosts.java @@ -0,0 +1,894 @@ + +package com.trilead.ssh2; + +import java.io.BufferedReader; +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.crypto.keys.Ed25519PublicKey; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.Ed25519Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; +import com.trilead.ssh2.signature.RSASHA256Verify; +import com.trilead.ssh2.signature.RSASHA512Verify; +import com.trilead.ssh2.transport.KexManager; + +/** + * The KnownHosts class is a handy tool to verify received server hostkeys + * based on the information in known_hosts files (the ones used by OpenSSH). + *

+ * It offers basically an in-memory database for known_hosts entries, as well as some + * helper functions. Entries from a known_hosts file can be loaded at construction time. + * It is also possible to add more keys later (e.g., one can parse different + * known_hosts files). + *

+ * It is a thread safe implementation, therefore, you need only to instantiate one + * KnownHosts for your whole application. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KnownHosts.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class KnownHosts +{ + public static final int HOSTKEY_IS_OK = 0; + public static final int HOSTKEY_IS_NEW = 1; + public static final int HOSTKEY_HAS_CHANGED = 2; + + protected class KnownHostsEntry + { + String[] patterns; + PublicKey key; + + KnownHostsEntry(String[] patterns, PublicKey key) + { + this.patterns = patterns; + this.key = key; + } + + @Override + public String toString() { + return "KnownHostsEntry{keyType=" + key.getAlgorithm() + "}"; + } + } + + protected final LinkedList publicKeys = new LinkedList<>(); + + public KnownHosts() + { + } + + public KnownHosts(char[] knownHostsData) throws IOException + { + initialize(knownHostsData); + } + + public KnownHosts(File knownHosts) throws IOException + { + initialize(knownHosts); + } + + /** + * Adds a single public key entry to the database. Note: this will NOT add the public key + * to any physical file (e.g., "~/.ssh/known_hosts") - use addHostkeyToFile() for that purpose. + * This method is designed to be used in a {@link ServerHostKeyVerifier}. + * + * @param hostnames a list of hostname patterns - at least one most be specified. Check out the + * OpenSSH sshd man page for a description of the pattern matching algorithm. + * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. + * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. + * @throws IOException + */ + public void addHostkey(String[] hostnames, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException + { + if (hostnames == null) + throw new IllegalArgumentException("hostnames may not be null"); + + if (RSASHA1Verify.ID_SSH_RSA.equals(serverHostKeyAlgorithm) || + RSASHA512Verify.ID_RSA_SHA_2_512.equals(serverHostKeyAlgorithm) || + RSASHA256Verify.ID_RSA_SHA_2_256.equals(serverHostKeyAlgorithm)) + { + PublicKey rpk = RSASHA1Verify.get().decodePublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, rpk)); + } + } else if (serverHostKeyAlgorithm.equals(DSASHA1Verify.ID_SSH_DSS)) { + PublicKey dpk = DSASHA1Verify.get().decodePublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, dpk)); + } + } else if (serverHostKeyAlgorithm.equals(ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().getKeyFormat())) { + PublicKey epk = ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().decodePublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, epk)); + } + } else if (serverHostKeyAlgorithm.equals(ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().getKeyFormat())) { + PublicKey epk = ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().decodePublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, epk)); + } + } else if (serverHostKeyAlgorithm.equals(ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().getKeyFormat())) { + PublicKey epk = ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().decodePublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, epk)); + } + } else if (Ed25519Verify.ED25519_ID.equals(serverHostKeyAlgorithm)) { + PublicKey edpk = Ed25519Verify.get().decodePublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, edpk)); + } + } else { + throw new IOException("Unknown host key type (" + serverHostKeyAlgorithm + ")"); + } + } + + /** + * Parses the given known_hosts data and adds entries to the database. + * + * @param knownHostsData + * @throws IOException + */ + public void addHostkeys(char[] knownHostsData) throws IOException + { + initialize(knownHostsData); + } + + /** + * Parses the given known_hosts file and adds entries to the database. + * + * @param knownHosts + * @throws IOException + */ + public void addHostkeys(File knownHosts) throws IOException + { + initialize(knownHosts); + } + + /** + * Generate the hashed representation of the given hostname. Useful for adding entries + * with hashed hostnames to a known_hosts file. (see -H option of OpenSSH key-gen). + * + * @param hostname + * @return the hashed representation, e.g., "|1|cDhrv7zwEUV3k71CEPHnhHZezhA=|Xo+2y6rUXo2OIWRAYhBOIijbJMA=" + */ + public static final String createHashedHostname(String hostname) + { + MessageDigest sha1; + try { + sha1 = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("VM doesn't support SHA1", e); + } + + byte[] salt = new byte[sha1.getDigestLength()]; + + new SecureRandom().nextBytes(salt); + + byte[] hash = hmacSha1Hash(salt, hostname); + + String base64_salt = new String(Base64.encode(salt)); + String base64_hash = new String(Base64.encode(hash)); + + return new String("|1|" + base64_salt + "|" + base64_hash); + } + + private static final byte[] hmacSha1Hash(byte[] salt, String hostname) + { + Mac hmac; + try { + hmac = Mac.getInstance("HmacSHA1"); + if (salt.length != hmac.getMacLength()) + throw new IllegalArgumentException("Salt has wrong length (" + salt.length + ")"); + hmac.init(new SecretKeySpec(salt, "HmacSHA1")); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to HMAC-SHA1", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("Unable to create SecretKey", e); + } + + try { + hmac.update(hostname.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + hmac.update(hostname.getBytes()); + } + + return hmac.doFinal(); + } + + private final boolean checkHashed(String entry, String hostname) + { + if (!entry.startsWith("|1|")) + return false; + + int delim_idx = entry.indexOf('|', 3); + + if (delim_idx == -1) + return false; + + String salt_base64 = entry.substring(3, delim_idx); + String hash_base64 = entry.substring(delim_idx + 1); + + byte[] salt = null; + byte[] hash = null; + + try + { + salt = Base64.decode(salt_base64.toCharArray()); + hash = Base64.decode(hash_base64.toCharArray()); + } + catch (IOException e) + { + return false; + } + + try { + MessageDigest sha1 = MessageDigest.getInstance("SHA1"); + if (salt.length != sha1.getDigestLength()) + return false; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("VM does not support SHA1", e); + } + + byte[] dig = hmacSha1Hash(salt, hostname); + + for (int i = 0; i < dig.length; i++) + if (dig[i] != hash[i]) + return false; + + return true; + } + + private int checkKey(String remoteHostname, PublicKey remoteKey) + { + int result = HOSTKEY_IS_NEW; + + synchronized (publicKeys) + { + Iterator i = publicKeys.iterator(); + + while (i.hasNext()) + { + KnownHostsEntry ke = i.next(); + + if (!hostnameMatches(ke.patterns, remoteHostname)) + continue; + + boolean res = matchKeys(ke.key, remoteKey); + + if (res) + return HOSTKEY_IS_OK; + + result = HOSTKEY_HAS_CHANGED; + } + } + return result; + } + + private List getAllKeys(String hostname) + { + List keys = new ArrayList<>(); + + synchronized (publicKeys) + { + Iterator i = publicKeys.iterator(); + + while (i.hasNext()) + { + KnownHostsEntry ke = i.next(); + + if (!hostnameMatches(ke.patterns, hostname)) + continue; + + keys.add(ke.key); + } + } + + return keys; + } + + /** + * Try to find the preferred order of hostkey algorithms for the given hostname. + * Based on the type of hostkey that is present in the internal database + * (i.e., either ssh-rsa or ssh-dss) + * an ordered list of hostkey algorithms is returned which can be passed + * to Connection.setServerHostKeyAlgorithms. + * + * @param hostname + * @return null if no key for the given hostname is present or + * there are keys of multiple types present for the given hostname. Otherwise, + * an array with hostkey algorithms is returned (i.e., an array of length 2). + */ + public String[] getPreferredServerHostkeyAlgorithmOrder(String hostname) + { + String[] algos = recommendHostkeyAlgorithms(hostname); + + if (algos != null) + return algos; + + InetAddress[] ipAddresses; + + try + { + ipAddresses = InetAddress.getAllByName(hostname); + } catch (UnknownHostException e) { + return null; + } + + for (InetAddress ipAddress : ipAddresses) { + algos = recommendHostkeyAlgorithms(ipAddress.getHostAddress()); + + if (algos != null) + return algos; + } + + return null; + } + + private final boolean hostnameMatches(String[] hostpatterns, String hostname) + { + boolean isMatch = false; + boolean negate = false; + + hostname = hostname.toLowerCase(Locale.US); + + for (int k = 0; k < hostpatterns.length; k++) + { + if (hostpatterns[k] == null) + continue; + + String pattern = null; + + /* In contrast to OpenSSH we also allow negated hash entries (as well as hashed + * entries in lines with multiple entries). + */ + + if ((hostpatterns[k].length() > 0) && (hostpatterns[k].charAt(0) == '!')) + { + pattern = hostpatterns[k].substring(1); + negate = true; + } + else + { + pattern = hostpatterns[k]; + negate = false; + } + + /* Optimize, no need to check this entry */ + + if ((isMatch) && (!negate)) + continue; + + /* Now compare */ + + if (pattern.charAt(0) == '|') + { + if (checkHashed(pattern, hostname)) + { + if (negate) + return false; + isMatch = true; + } + } + else + { + pattern = pattern.toLowerCase(Locale.US); + + if ((pattern.indexOf('?') != -1) || (pattern.indexOf('*') != -1)) + { + if (pseudoRegex(pattern.toCharArray(), 0, hostname.toCharArray(), 0)) + { + if (negate) + return false; + isMatch = true; + } + } + else if (pattern.compareTo(hostname) == 0) + { + if (negate) + return false; + isMatch = true; + } + } + } + + return isMatch; + } + + private void initialize(char[] knownHostsData) throws IOException + { + BufferedReader br = new BufferedReader(new CharArrayReader(knownHostsData)); + + while (true) + { + String line = br.readLine(); + + if (line == null) + break; + + line = line.trim(); + + if (line.startsWith("#")) + continue; + + String[] arr = line.split(" "); + + if (arr.length >= 3) + { + String[] hostnames = arr[0].split(","); + + byte[] msg = Base64.decode(arr[2].toCharArray()); + + addHostkey(hostnames, arr[1], msg); + } + } + } + + private void initialize(File knownHosts) throws IOException + { + char[] buff = new char[512]; + + CharArrayWriter cw = new CharArrayWriter(); + + knownHosts.createNewFile(); + + FileReader fr = new FileReader(knownHosts); + + while (true) + { + int len = fr.read(buff); + if (len < 0) + break; + cw.write(buff, 0, len); + } + + fr.close(); + + initialize(cw.toCharArray()); + } + + private final boolean matchKeys(PublicKey key1, PublicKey key2) + { + return key1.equals(key2); + } + + private final boolean pseudoRegex(char[] pattern, int i, char[] match, int j) + { + /* This matching logic is equivalent to the one present in OpenSSH 4.1 */ + + while (true) + { + /* Are we at the end of the pattern? */ + + if (pattern.length == i) + return (match.length == j); + + if (pattern[i] == '*') + { + i++; + + if (pattern.length == i) + return true; + + if ((pattern[i] != '*') && (pattern[i] != '?')) + { + while (true) + { + if ((pattern[i] == match[j]) && pseudoRegex(pattern, i + 1, match, j + 1)) + return true; + j++; + if (match.length == j) + return false; + } + } + + while (true) + { + if (pseudoRegex(pattern, i, match, j)) + return true; + j++; + if (match.length == j) + return false; + } + } + + if (match.length == j) + return false; + + if ((pattern[i] != '?') && (pattern[i] != match[j])) + return false; + + i++; + j++; + } + } + + private final String[] ALGOS_FOR_RSA = new String[] { + RSASHA512Verify.ID_RSA_SHA_2_512, + RSASHA256Verify.ID_RSA_SHA_2_256, + RSASHA1Verify.ID_SSH_RSA, + }; + + private final String ALGO_FOR_DSS = DSASHA1Verify.ID_SSH_DSS; + + private final String ALGO_FOR_EDDSA = Ed25519Verify.ED25519_ID; + + private String[] recommendHostkeyAlgorithms(String hostname) { + List preferredAlgos = new ArrayList<>(); + + List keys = getAllKeys(hostname); + + for (PublicKey key : keys) { + if (key instanceof RSAPublicKey) { + preferredAlgos.addAll(Arrays.asList(ALGOS_FOR_RSA)); + } else if (key instanceof DSAPublicKey) { + preferredAlgos.add(ALGO_FOR_DSS); + } else if (key instanceof Ed25519PublicKey) { + preferredAlgos.add(ALGO_FOR_EDDSA); + } else if (key instanceof ECPublicKey) { + preferredAlgos.add(ECDSASHA2Verify.getSshKeyType((ECPublicKey) key)); + } + } + + /* If we did not find anything that we know of, return null */ + if (preferredAlgos.isEmpty()) + return null; + + /* Now put the preferred algo to the start of the array. + * You may ask yourself why we do it that way - basically, we could just + * return only the preferred algorithm: since we have a saved key of that + * type (sent earlier from the remote host), then that should work out. + * However, imagine that the server is (for whatever reasons) not offering + * that type of hostkey anymore (e.g., "ssh-rsa" was disabled and + * now "ssh-dss" is being used). If we then do not let the server send us + * a fresh key of the new type, then we shoot ourself into the foot: + * the connection cannot be established and hence the user cannot decide + * if he/she wants to accept the new key. + */ + + List preferredAndOthers = new ArrayList<>(); + List notPreferred = new ArrayList<>(); + for (String algo : KexManager.getDefaultServerHostkeyAlgorithmList()) { + if (preferredAlgos.contains(algo)) { + preferredAndOthers.add(algo); + } else { + notPreferred.add(algo); + } + } + preferredAndOthers.addAll(notPreferred); + return preferredAndOthers.toArray(new String[0]); + } + + /** + * Checks the internal hostkey database for the given hostkey. + * If no matching key can be found, then the hostname is resolved to an IP address + * and the search is repeated using that IP address. + * + * @param hostname the server's hostname, will be matched with all hostname patterns + * @param serverHostKeyAlgorithm type of hostkey, either ssh-rsa or ssh-dss + * @param serverHostKey the key blob + * @return

    + *
  • HOSTKEY_IS_OK: the given hostkey matches an entry for the given hostname
  • + *
  • HOSTKEY_IS_NEW: no entries found for this hostname and this type of hostkey
  • + *
  • HOSTKEY_HAS_CHANGED: hostname is known, but with another key of the same type + * (man-in-the-middle attack?)
  • + *
+ * @throws IOException if the supplied key blob cannot be parsed or does not match the given hostkey type. + */ + public int verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException + { + PublicKey remoteKey = null; + + if (RSASHA1Verify.ID_SSH_RSA.equals(serverHostKeyAlgorithm) || + RSASHA256Verify.ID_RSA_SHA_2_256.equals(serverHostKeyAlgorithm) || + RSASHA512Verify.ID_RSA_SHA_2_512.equals(serverHostKeyAlgorithm)) + { + remoteKey = RSASHA1Verify.get().decodePublicKey(serverHostKey); + } + else if (DSASHA1Verify.ID_SSH_DSS.equals(serverHostKeyAlgorithm)) + { + remoteKey = DSASHA1Verify.get().decodePublicKey(serverHostKey); + } + else if (ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().getKeyFormat().equals(serverHostKeyAlgorithm)) + { + remoteKey = ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().decodePublicKey(serverHostKey); + } + else if (ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().getKeyFormat().equals(serverHostKeyAlgorithm)) + { + remoteKey = ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().decodePublicKey(serverHostKey); + } + else if (ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().getKeyFormat().equals(serverHostKeyAlgorithm)) + { + remoteKey = ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().decodePublicKey(serverHostKey); + } + else if (Ed25519Verify.ED25519_ID.equals(serverHostKeyAlgorithm)) + { + remoteKey = Ed25519Verify.get().decodePublicKey(serverHostKey); + } + else + throw new IllegalArgumentException("Unknown hostkey type " + serverHostKeyAlgorithm); + + int result = checkKey(hostname, remoteKey); + + if (result == HOSTKEY_IS_OK) + return result; + + InetAddress[] ipAddresses = null; + + try + { + ipAddresses = InetAddress.getAllByName(hostname); + } + catch (UnknownHostException e) + { + return result; + } + + for (InetAddress ipAddress : ipAddresses) { + int newresult = checkKey(ipAddress.getHostAddress(), remoteKey); + + if (newresult == HOSTKEY_IS_OK) + return newresult; + + if (newresult == HOSTKEY_HAS_CHANGED) + result = HOSTKEY_HAS_CHANGED; + } + + return result; + } + + /** + * Adds a single public key entry to the a known_hosts file. + * This method is designed to be used in a {@link ServerHostKeyVerifier}. + * + * @param knownHosts the file where the publickey entry will be appended. + * @param hostnames a list of hostname patterns - at least one most be specified. Check out the + * OpenSSH sshd man page for a description of the pattern matching algorithm. + * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. + * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. + * @throws IOException + */ + public final static void addHostkeyToFile(File knownHosts, String[] hostnames, String serverHostKeyAlgorithm, + byte[] serverHostKey) throws IOException + { + if ((hostnames == null) || (hostnames.length == 0)) + throw new IllegalArgumentException("Need at least one hostname specification"); + + if ((serverHostKeyAlgorithm == null) || (serverHostKey == null)) + throw new IllegalArgumentException(); + + CharArrayWriter writer = new CharArrayWriter(); + + for (int i = 0; i < hostnames.length; i++) + { + if (i != 0) + writer.write(','); + writer.write(hostnames[i]); + } + + writer.write(' '); + writer.write(serverHostKeyAlgorithm); + writer.write(' '); + writer.write(Base64.encode(serverHostKey)); + writer.write("\n"); + + char[] entry = writer.toCharArray(); + + RandomAccessFile raf = new RandomAccessFile(knownHosts, "rw"); + + long len = raf.length(); + + if (len > 0) + { + raf.seek(len - 1); + int last = raf.read(); + if (last != '\n') + raf.write('\n'); + } + + try { + raf.write(new String(entry).getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + raf.write(new String(entry).getBytes()); + } + raf.close(); + } + + /** + * Generates a "raw" fingerprint of a hostkey. + * + * @param type either "md5" or "sha1" + * @param keyType either "ssh-rsa" or "ssh-dss" + * @param hostkey the hostkey + * @return the raw fingerprint + */ + private static byte[] rawFingerPrint(String type, String keyType, byte[] hostkey) + { + MessageDigest dig = null; + + try { + if ("md5".equals(type)) + { + dig = MessageDigest.getInstance("MD5"); + } + else if ("sha1".equals(type)) + { + dig = MessageDigest.getInstance("SHA1"); + } + else + { + throw new IllegalArgumentException("Unknown hash type " + type); + } + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unknown hash type " + type); + } + + if (Ed25519Verify.ED25519_ID.equals(keyType)) + { + } + else if (keyType.startsWith(ECDSASHA2Verify.ECDSA_SHA2_PREFIX)) + { + } + else if (RSASHA1Verify.ID_SSH_RSA.equals(keyType)) + { + } + else if (DSASHA1Verify.ID_SSH_DSS.equals(keyType)) + { + } + else if (RSASHA256Verify.ID_RSA_SHA_2_256.equals(keyType)) + { + } + else if (RSASHA512Verify.ID_RSA_SHA_2_512.equals(keyType)) + { + } + else + throw new IllegalArgumentException("Unknown key type " + keyType); + + if (hostkey == null) + throw new IllegalArgumentException("hostkey is null"); + + dig.update(hostkey); + return dig.digest(); + } + + /** + * Convert a raw fingerprint to hex representation (XX:YY:ZZ...). + * @param fingerprint raw fingerprint + * @return the hex representation + */ + private static String rawToHexFingerprint(byte[] fingerprint) + { + final char[] alpha = "0123456789abcdef".toCharArray(); + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < fingerprint.length; i++) + { + if (i != 0) + sb.append(':'); + int b = fingerprint[i] & 0xff; + sb.append(alpha[b >> 4]); + sb.append(alpha[b & 15]); + } + + return sb.toString(); + } + + /** + * Convert a raw fingerprint to bubblebabble representation. + * @param raw raw fingerprint + * @return the bubblebabble representation + */ + static final private String rawToBubblebabbleFingerprint(byte[] raw) + { + final char[] v = "aeiouy".toCharArray(); + final char[] c = "bcdfghklmnprstvzx".toCharArray(); + + StringBuilder sb = new StringBuilder(); + + int seed = 1; + + int rounds = (raw.length / 2) + 1; + + sb.append('x'); + + for (int i = 0; i < rounds; i++) + { + if (((i + 1) < rounds) || ((raw.length) % 2 != 0)) + { + sb.append(v[(((raw[2 * i] >> 6) & 3) + seed) % 6]); + sb.append(c[(raw[2 * i] >> 2) & 15]); + sb.append(v[((raw[2 * i] & 3) + (seed / 6)) % 6]); + + if ((i + 1) < rounds) + { + sb.append(c[(((raw[(2 * i) + 1])) >> 4) & 15]); + sb.append('-'); + sb.append(c[(((raw[(2 * i) + 1]))) & 15]); + // As long as seed >= 0, seed will be >= 0 afterwards + seed = ((seed * 5) + (((raw[2 * i] & 0xff) * 7) + (raw[(2 * i) + 1] & 0xff))) % 36; + } + } + else + { + sb.append(v[seed % 6]); // seed >= 0, therefore index positive + sb.append('x'); + sb.append(v[seed / 6]); + } + } + + sb.append('x'); + + return sb.toString(); + } + + /** + * Convert a ssh2 key-blob into a human readable hex fingerprint. + * Generated fingerprints are identical to those generated by OpenSSH. + *

+ * Example fingerprint: d0:cb:76:19:99:5a:03:fc:73:10:70:93:f2:44:63:47. + + * @param keytype either "ssh-rsa" or "ssh-dss" + * @param publickey key blob + * @return Hex fingerprint + */ + public final static String createHexFingerprint(String keytype, byte[] publickey) + { + byte[] raw = rawFingerPrint("md5", keytype, publickey); + return rawToHexFingerprint(raw); + } + + /** + * Convert a ssh2 key-blob into a human readable bubblebabble fingerprint. + * The used bubblebabble algorithm (taken from OpenSSH) generates fingerprints + * that are easier to remember for humans. + *

+ * Example fingerprint: xofoc-bubuz-cazin-zufyl-pivuk-biduk-tacib-pybur-gonar-hotat-lyxux. + * + * @param keytype either "ssh-rsa" or "ssh-dss" + * @param publickey key data + * @return Bubblebabble fingerprint + */ + public final static String createBubblebabbleFingerprint(String keytype, byte[] publickey) + { + byte[] raw = rawFingerPrint("sha1", keytype, publickey); + return rawToBubblebabbleFingerprint(raw); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalPortForwarder.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalPortForwarder.java new file mode 100644 index 0000000000..6f42d5ad27 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalPortForwarder.java @@ -0,0 +1,61 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.LocalAcceptThread; + + +/** + * A LocalPortForwarder forwards TCP/IP connections to a local + * port via the secure tunnel to another host (which may or may not be identical + * to the remote SSH-2 server). Checkout {@link Connection#createLocalPortForwarder(int, String, int)} + * on how to create one. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: LocalPortForwarder.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class LocalPortForwarder +{ + ChannelManager cm; + + String host_to_connect; + + int port_to_connect; + + LocalAcceptThread lat; + + LocalPortForwarder(ChannelManager cm, int local_port, String host_to_connect, int port_to_connect) + throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + lat = new LocalAcceptThread(cm, local_port, host_to_connect, port_to_connect); + lat.setDaemon(true); + lat.start(); + } + + LocalPortForwarder(ChannelManager cm, InetSocketAddress addr, String host_to_connect, int port_to_connect) + throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + lat = new LocalAcceptThread(cm, addr, host_to_connect, port_to_connect); + lat.setDaemon(true); + lat.start(); + } + + /** + * Stop TCP/IP forwarding of newly arriving connections. + * + */ + public void close() { + lat.stopWorking(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java new file mode 100644 index 0000000000..ce5c501ecc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java @@ -0,0 +1,74 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.trilead.ssh2.channel.Channel; +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.LocalAcceptThread; + + +/** + * A LocalStreamForwarder forwards an Input- and Outputstream + * pair via the secure tunnel to another host (which may or may not be identical + * to the remote SSH-2 server). + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: LocalStreamForwarder.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class LocalStreamForwarder +{ + ChannelManager cm; + + String host_to_connect; + int port_to_connect; + LocalAcceptThread lat; + + Channel cn; + + LocalStreamForwarder(ChannelManager cm, String host_to_connect, int port_to_connect) throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + cn = cm.openDirectTCPIPChannel(host_to_connect, port_to_connect, "127.0.0.1", 0); + } + + /** + * @return An InputStream object. + */ + public InputStream getInputStream() { + return cn.getStdoutStream(); + } + + /** + * Get the OutputStream. Please be aware that the implementation MAY use an + * internal buffer. To make sure that the buffered data is sent over the + * tunnel, you have to call the flush method of the + * OutputStream. To signal EOF, please use the + * close method of the OutputStream. + * + * @return An OutputStream object. + */ + public OutputStream getOutputStream() { + return cn.getStdinStream(); + } + + /** + * Close the underlying SSH forwarding channel and free up resources. + * You can also use this method to force the shutdown of the underlying + * forwarding channel. Pending output (OutputStream not flushed) will NOT + * be sent. Pending input (InputStream) can still be read. If the shutdown + * operation is already in progress (initiated from either side), then this + * call is a no-op. + * + * @throws IOException + */ + public void close() throws IOException + { + cm.closeChannel(cn, "Closed due to user request.", true); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ProxyData.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ProxyData.java new file mode 100644 index 0000000000..9b240bca36 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ProxyData.java @@ -0,0 +1,28 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.net.Socket; + +/** + * An abstract interface implemented by all proxy data implementations. + * + * @see HTTPProxyData + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ProxyData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface ProxyData +{ + /** + * Connects the socket to the given destination using the proxy method that this instance + * represents. + * @param hostname hostname of end host (not proxy) + * @param port port of end host (not proxy) + * @param connectTimeout number of seconds before giving up on connecting to end host + * @throws IOException if the connection could not be completed + * @return connected socket instance + */ + Socket openConnection(String hostname, int port, int connectTimeout) throws IOException; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SCPClient.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SCPClient.java new file mode 100644 index 0000000000..71345da6ad --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SCPClient.java @@ -0,0 +1,747 @@ + +package com.trilead.ssh2; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +/** + * A very basic SCPClient that can be used to copy files from/to + * the SSH-2 server. On the server side, the "scp" program must be in the PATH. + *

+ * This scp client is thread safe - you can download (and upload) different sets + * of files concurrently without any troubles. The SCPClient is + * actually mapping every request to a distinct {@link Session}. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SCPClient.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class SCPClient +{ + Connection conn; + + class LenNamePair + { + long length; + String filename; + } + + public SCPClient(Connection conn) + { + if (conn == null) + throw new IllegalArgumentException("Cannot accept null argument!"); + this.conn = conn; + } + + private void readResponse(InputStream is) throws IOException + { + int c = is.read(); + + if (c == 0) + return; + + if (c == -1) + throw new IOException("Remote scp terminated unexpectedly."); + + if ((c != 1) && (c != 2)) + throw new IOException("Remote scp sent illegal error code."); + + if (c == 2) + throw new IOException("Remote scp terminated with error."); + + String err = receiveLine(is); + throw new IOException("Remote scp terminated with error (" + err + ")."); + } + + private String receiveLine(InputStream is) throws IOException + { + StringBuffer sb = new StringBuffer(30); + + while (true) + { + /* + * This is a random limit - if your path names are longer, then + * adjust it + */ + + if (sb.length() > 8192) + throw new IOException("Remote scp sent a too long line"); + + int c = is.read(); + + if (c < 0) + throw new IOException("Remote scp terminated unexpectedly."); + + if (c == '\n') + break; + + sb.append((char) c); + + } + return sb.toString(); + } + + private LenNamePair parseCLine(String line) throws IOException + { + /* Minimum line: "xxxx y z" ---> 8 chars */ + + long len; + + if (line.length() < 8) + throw new IOException("Malformed C line sent by remote SCP binary, line too short."); + + if ((line.charAt(4) != ' ') || (line.charAt(5) == ' ')) + throw new IOException("Malformed C line sent by remote SCP binary."); + + int length_name_sep = line.indexOf(' ', 5); + + if (length_name_sep == -1) + throw new IOException("Malformed C line sent by remote SCP binary."); + + String length_substring = line.substring(5, length_name_sep); + String name_substring = line.substring(length_name_sep + 1); + + if ((length_substring.length() <= 0) || (name_substring.length() <= 0)) + throw new IOException("Malformed C line sent by remote SCP binary."); + + if ((6 + length_substring.length() + name_substring.length()) != line.length()) + throw new IOException("Malformed C line sent by remote SCP binary."); + + try + { + len = Long.parseLong(length_substring); + } + catch (NumberFormatException e) + { + throw new IOException("Malformed C line sent by remote SCP binary, cannot parse file length."); + } + + if (len < 0) + throw new IOException("Malformed C line sent by remote SCP binary, illegal file length."); + + LenNamePair lnp = new LenNamePair(); + lnp.length = len; + lnp.filename = name_substring; + + return lnp; + } + + private void sendBytes(Session sess, byte[] data, String fileName, String mode) throws IOException + { + OutputStream os = sess.getStdin(); + InputStream is = new BufferedInputStream(sess.getStdout(), 512); + + readResponse(is); + + String cline = "C" + mode + " " + data.length + " " + fileName + "\n"; + + try { + os.write(cline.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + os.write(cline.getBytes()); + } + + os.flush(); + + readResponse(is); + + os.write(data, 0, data.length); + os.write(0); + os.flush(); + + readResponse(is); + + try { + os.write("E\n".getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + os.write("E\n".getBytes()); + } + os.flush(); + } + + private void sendFiles(Session sess, String[] files, String[] remoteFiles, String mode) throws IOException + { + byte[] buffer = new byte[8192]; + + OutputStream os = new BufferedOutputStream(sess.getStdin(), 40000); + InputStream is = new BufferedInputStream(sess.getStdout(), 512); + + readResponse(is); + + for (int i = 0; i < files.length; i++) + { + File f = new File(files[i]); + long remain = f.length(); + + String remoteName; + + if ((remoteFiles != null) && (remoteFiles.length > i) && (remoteFiles[i] != null)) + remoteName = remoteFiles[i]; + else + remoteName = f.getName(); + + String cline = "C" + mode + " " + remain + " " + remoteName + "\n"; + + try { + os.write(cline.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + os.write(cline.getBytes()); + } + os.flush(); + + readResponse(is); + + FileInputStream fis = null; + + try + { + fis = new FileInputStream(f); + + while (remain > 0) + { + int trans; + if (remain > buffer.length) + trans = buffer.length; + else + trans = (int) remain; + + if (fis.read(buffer, 0, trans) != trans) + throw new IOException("Cannot read enough from local file " + files[i]); + + os.write(buffer, 0, trans); + + remain -= trans; + } + } + finally + { + if (fis != null) + fis.close(); + } + + os.write(0); + os.flush(); + + readResponse(is); + } + + try { + os.write("E\n".getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + os.write("E\n".getBytes("ISO-8859-1")); + } + os.flush(); + } + + private void receiveFiles(Session sess, OutputStream[] targets) throws IOException + { + byte[] buffer = new byte[8192]; + + OutputStream os = new BufferedOutputStream(sess.getStdin(), 512); + InputStream is = new BufferedInputStream(sess.getStdout(), 40000); + + os.write(0x0); + os.flush(); + + for (int i = 0; i < targets.length; i++) + { + LenNamePair lnp = null; + + while (true) + { + int c = is.read(); + if (c < 0) + throw new IOException("Remote scp terminated unexpectedly."); + + String line = receiveLine(is); + + if (c == 'T') + { + /* Ignore modification times */ + + continue; + } + + if ((c == 1) || (c == 2)) + throw new IOException("Remote SCP error: " + line); + + if (c == 'C') + { + lnp = parseCLine(line); + break; + + } + throw new IOException("Remote SCP error: " + ((char) c) + line); + } + + os.write(0x0); + os.flush(); + + long remain = lnp.length; + + while (remain > 0) + { + int trans; + if (remain > buffer.length) + trans = buffer.length; + else + trans = (int) remain; + + int this_time_received = is.read(buffer, 0, trans); + + if (this_time_received < 0) + { + throw new IOException("Remote scp terminated connection unexpectedly"); + } + + targets[i].write(buffer, 0, this_time_received); + + remain -= this_time_received; + } + + readResponse(is); + + os.write(0x0); + os.flush(); + } + } + + private void receiveFiles(Session sess, String[] files, String target) throws IOException + { + byte[] buffer = new byte[8192]; + + OutputStream os = new BufferedOutputStream(sess.getStdin(), 512); + InputStream is = new BufferedInputStream(sess.getStdout(), 40000); + + os.write(0x0); + os.flush(); + + for (int i = 0; i < files.length; i++) + { + LenNamePair lnp = null; + + while (true) + { + int c = is.read(); + if (c < 0) + throw new IOException("Remote scp terminated unexpectedly."); + + String line = receiveLine(is); + + if (c == 'T') + { + /* Ignore modification times */ + + continue; + } + + if ((c == 1) || (c == 2)) + throw new IOException("Remote SCP error: " + line); + + if (c == 'C') + { + lnp = parseCLine(line); + break; + + } + throw new IOException("Remote SCP error: " + ((char) c) + line); + } + + os.write(0x0); + os.flush(); + + File f = new File(target + File.separatorChar + lnp.filename); + FileOutputStream fop = null; + + try + { + fop = new FileOutputStream(f); + + long remain = lnp.length; + + while (remain > 0) + { + int trans; + if (remain > buffer.length) + trans = buffer.length; + else + trans = (int) remain; + + int this_time_received = is.read(buffer, 0, trans); + + if (this_time_received < 0) + { + throw new IOException("Remote scp terminated connection unexpectedly"); + } + + fop.write(buffer, 0, this_time_received); + + remain -= this_time_received; + } + } + finally + { + if (fop != null) + fop.close(); + } + + readResponse(is); + + os.write(0x0); + os.flush(); + } + } + + /** + * Copy a local file to a remote directory, uses mode 0600 when creating the + * file on the remote side. + * + * @param localFile + * Path and name of local file. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * + * @throws IOException + */ + public void put(String localFile, String remoteTargetDirectory) throws IOException + { + put(new String[] { localFile }, remoteTargetDirectory, "0600"); + } + + /** + * Copy a set of local files to a remote directory, uses mode 0600 when + * creating files on the remote side. + * + * @param localFiles + * Paths and names of local file names. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * + * @throws IOException + */ + + public void put(String[] localFiles, String remoteTargetDirectory) throws IOException + { + put(localFiles, remoteTargetDirectory, "0600"); + } + + /** + * Copy a local file to a remote directory, uses the specified mode when + * creating the file on the remote side. + * + * @param localFile + * Path and name of local file. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(String localFile, String remoteTargetDirectory, String mode) throws IOException + { + put(new String[] { localFile }, remoteTargetDirectory, mode); + } + + /** + * Copy a local file to a remote directory, uses the specified mode and + * remote filename when creating the file on the remote side. + * + * @param localFile + * Path and name of local file. + * @param remoteFileName + * The name of the file which will be created in the remote + * target directory. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(String localFile, String remoteFileName, String remoteTargetDirectory, String mode) + throws IOException + { + put(new String[] { localFile }, new String[] { remoteFileName }, remoteTargetDirectory, mode); + } + + /** + * Create a remote file and copy the contents of the passed byte array into + * it. Uses mode 0600 for creating the remote file. + * + * @param data + * the data to be copied into the remote file. + * @param remoteFileName + * The name of the file which will be created in the remote + * target directory. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @throws IOException + */ + + public void put(byte[] data, String remoteFileName, String remoteTargetDirectory) throws IOException + { + put(data, remoteFileName, remoteTargetDirectory, "0600"); + } + + /** + * Create a remote file and copy the contents of the passed byte array into + * it. The method use the specified mode when creating the file on the + * remote side. + * + * @param data + * the data to be copied into the remote file. + * @param remoteFileName + * The name of the file which will be created in the remote + * target directory. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(byte[] data, String remoteFileName, String remoteTargetDirectory, String mode) throws IOException + { + Session sess = null; + + if ((remoteFileName == null) || (remoteTargetDirectory == null) || (mode == null)) + throw new IllegalArgumentException("Null argument."); + + if (mode.length() != 4) + throw new IllegalArgumentException("Invalid mode."); + + for (int i = 0; i < mode.length(); i++) + if (!Character.isDigit(mode.charAt(i))) + throw new IllegalArgumentException("Invalid mode."); + + remoteTargetDirectory = remoteTargetDirectory.trim(); + remoteTargetDirectory = (remoteTargetDirectory.length() > 0) ? remoteTargetDirectory : "."; + + String cmd = "scp -t -d " + remoteTargetDirectory; + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + sendBytes(sess, data, remoteFileName, mode); + } + catch (IOException e) + { + throw new IOException("Error during SCP transfer.", e); + } + finally + { + if (sess != null) + sess.close(); + } + } + + /** + * Copy a set of local files to a remote directory, uses the specified mode + * when creating the files on the remote side. + * + * @param localFiles + * Paths and names of the local files. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(String[] localFiles, String remoteTargetDirectory, String mode) throws IOException + { + put(localFiles, null, remoteTargetDirectory, mode); + } + + public void put(String[] localFiles, String[] remoteFiles, String remoteTargetDirectory, String mode) + throws IOException + { + Session sess = null; + + /* + * remoteFiles may be null, indicating that the local filenames shall be + * used + */ + + if ((localFiles == null) || (remoteTargetDirectory == null) || (mode == null)) + throw new IllegalArgumentException("Null argument."); + + if (mode.length() != 4) + throw new IllegalArgumentException("Invalid mode."); + + for (int i = 0; i < mode.length(); i++) + if (!Character.isDigit(mode.charAt(i))) + throw new IllegalArgumentException("Invalid mode."); + + if (localFiles.length == 0) + return; + + remoteTargetDirectory = remoteTargetDirectory.trim(); + remoteTargetDirectory = (remoteTargetDirectory.length() > 0) ? remoteTargetDirectory : "."; + + String cmd = "scp -t -d " + remoteTargetDirectory; + + for (int i = 0; i < localFiles.length; i++) + { + if (localFiles[i] == null) + throw new IllegalArgumentException("Cannot accept null filename."); + } + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + sendFiles(sess, localFiles, remoteFiles, mode); + } + catch (IOException e) + { + throw new IOException("Error during SCP transfer.", e); + } + finally + { + if (sess != null) + sess.close(); + } + } + + /** + * Download a file from the remote server to a local directory. + * + * @param remoteFile + * Path and name of the remote file. + * @param localTargetDirectory + * Local directory to put the downloaded file. + * + * @throws IOException + */ + public void get(String remoteFile, String localTargetDirectory) throws IOException + { + get(new String[] { remoteFile }, localTargetDirectory); + } + + /** + * Download a file from the remote server and pipe its contents into an + * OutputStream. Please note that, to enable flexible usage + * of this method, the OutputStream will not be closed nor + * flushed. + * + * @param remoteFile + * Path and name of the remote file. + * @param target + * OutputStream where the contents of the file will be sent to. + * @throws IOException + */ + public void get(String remoteFile, OutputStream target) throws IOException + { + get(new String[] { remoteFile }, new OutputStream[] { target }); + } + + private void get(String remoteFiles[], OutputStream[] targets) throws IOException + { + Session sess = null; + + if ((remoteFiles == null) || (targets == null)) + throw new IllegalArgumentException("Null argument."); + + if (remoteFiles.length != targets.length) + throw new IllegalArgumentException("Length of arguments does not match."); + + if (remoteFiles.length == 0) + return; + + String cmd = "scp -f"; + + for (int i = 0; i < remoteFiles.length; i++) + { + if (remoteFiles[i] == null) + throw new IllegalArgumentException("Cannot accept null filename."); + + String tmp = remoteFiles[i].trim(); + + if (tmp.length() == 0) + throw new IllegalArgumentException("Cannot accept empty filename."); + + cmd += (" " + tmp); + } + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + receiveFiles(sess, targets); + } + catch (IOException e) + { + throw new IOException("Error during SCP transfer.", e); + } + finally + { + if (sess != null) + sess.close(); + } + } + + /** + * Download a set of files from the remote server to a local directory. + * + * @param remoteFiles + * Paths and names of the remote files. + * @param localTargetDirectory + * Local directory to put the downloaded files. + * + * @throws IOException + */ + public void get(String remoteFiles[], String localTargetDirectory) throws IOException + { + Session sess = null; + + if ((remoteFiles == null) || (localTargetDirectory == null)) + throw new IllegalArgumentException("Null argument."); + + if (remoteFiles.length == 0) + return; + + String cmd = "scp -f"; + + for (int i = 0; i < remoteFiles.length; i++) + { + if (remoteFiles[i] == null) + throw new IllegalArgumentException("Cannot accept null filename."); + + String tmp = remoteFiles[i].trim(); + + if (tmp.length() == 0) + throw new IllegalArgumentException("Cannot accept empty filename."); + + cmd += (" " + tmp); + } + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + receiveFiles(sess, remoteFiles, localTargetDirectory); + } + catch (IOException e) + { + throw new IOException("Error during SCP transfer.", e); + } + finally + { + if (sess != null) + sess.close(); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPException.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPException.java new file mode 100644 index 0000000000..99a69d9943 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPException.java @@ -0,0 +1,91 @@ + +package com.trilead.ssh2; + +import java.io.IOException; + +import com.trilead.ssh2.sftp.ErrorCodes; + + +/** + * Used in combination with the SFTPv3Client. This exception wraps + * error messages sent by the SFTP server. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class SFTPException extends IOException +{ + private static final long serialVersionUID = 578654644222421811L; + + private final String sftpErrorMessage; + private final int sftpErrorCode; + + private static String constructMessage(String s, int errorCode) + { + String[] detail = ErrorCodes.getDescription(errorCode); + + if (detail == null) + return s + " (UNKNOW SFTP ERROR CODE)"; + + return s + " (" + detail[0] + ": " + detail[1] + ")"; + } + + SFTPException(String msg, int errorCode) + { + super(constructMessage(msg, errorCode)); + sftpErrorMessage = msg; + sftpErrorCode = errorCode; + } + + /** + * Get the error message sent by the server. Often, this + * message does not help a lot (e.g., "failure"). + * + * @return the plain string as sent by the server. + */ + public String getServerErrorMessage() + { + return sftpErrorMessage; + } + + /** + * Get the error code sent by the server. + * + * @return an error code as defined in the SFTP specs. + */ + public int getServerErrorCode() + { + return sftpErrorCode; + } + + /** + * Get the symbolic name of the error code as given in the SFTP specs. + * + * @return e.g., "SSH_FX_INVALID_FILENAME". + */ + public String getServerErrorCodeSymbol() + { + String[] detail = ErrorCodes.getDescription(sftpErrorCode); + + if (detail == null) + return "UNKNOW SFTP ERROR CODE " + sftpErrorCode; + + return detail[0]; + } + + /** + * Get the description of the error code as given in the SFTP specs. + * + * @return e.g., "The filename is not valid." + */ + public String getServerErrorCodeVerbose() + { + String[] detail = ErrorCodes.getDescription(sftpErrorCode); + + if (detail == null) + return "The error code " + sftpErrorCode + " is unknown."; + + return detail[1]; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3Client.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3Client.java new file mode 100644 index 0000000000..498300f6e1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3Client.java @@ -0,0 +1,1389 @@ + +package com.trilead.ssh2; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Vector; + +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.sftp.AttribFlags; +import com.trilead.ssh2.sftp.ErrorCodes; +import com.trilead.ssh2.sftp.Packet; + + +/** + * A SFTPv3Client represents a SFTP (protocol version 3) + * client connection tunnelled over a SSH-2 connection. This is a very simple + * (synchronous) implementation. + *

+ * Basically, most methods in this class map directly to one of + * the packet types described in draft-ietf-secsh-filexfer-02.txt. + *

+ * Note: this is experimental code. + *

+ * Error handling: the methods of this class throw IOExceptions. However, unless + * there is catastrophic failure, exceptions of the type {@link SFTPv3Client} will + * be thrown (a subclass of IOException). Therefore, you can implement more verbose + * behavior by checking if a thrown exception if of this type. If yes, then you + * can cast the exception and access detailed information about the failure. + *

+ * Notes about file names, directory names and paths, copy-pasted + * from the specs: + *

    + *
  • SFTP v3 represents file names as strings. File names are + * assumed to use the slash ('/') character as a directory separator.
  • + *
  • File names starting with a slash are "absolute", and are relative to + * the root of the file system. Names starting with any other character + * are relative to the user's default directory (home directory).
  • + *
  • Servers SHOULD interpret a path name component ".." as referring to + * the parent directory, and "." as referring to the current directory. + * If the server implementation limits access to certain parts of the + * file system, it must be extra careful in parsing file names when + * enforcing such restrictions. There have been numerous reported + * security bugs where a ".." in a path name has allowed access outside + * the intended area.
  • + *
  • An empty path name is valid, and it refers to the user's default + * directory (usually the user's home directory).
  • + *
+ *

+ * If you are still not tired then please go on and read the comment for + * {@link #setCharset(String)}. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3Client.java,v 1.3 2008/04/01 12:38:09 cplattne Exp $ + */ +public class SFTPv3Client +{ + final Connection conn; + final Session sess; + final PrintStream debug; + + boolean flag_closed = false; + + InputStream is; + OutputStream os; + + int protocol_version = 0; + HashMap server_extensions = new HashMap(); + + int next_request_id = 1000; + + String charsetName = null; + + /** + * Create a SFTP v3 client. + * + * @param conn The underlying SSH-2 connection to be used. + * @param debug + * @throws IOException + * + * @deprecated this constructor (debug version) will disappear in the future, + * use {@link #SFTPv3Client(Connection)} instead. + */ + @Deprecated + public SFTPv3Client(Connection conn, PrintStream debug) throws IOException + { + if (conn == null) + throw new IllegalArgumentException("Cannot accept null argument!"); + + this.conn = conn; + this.debug = debug; + + if (debug != null) + debug.println("Opening session and starting SFTP subsystem."); + + sess = conn.openSession(); + sess.startSubSystem("sftp"); + + is = sess.getStdout(); + os = new BufferedOutputStream(sess.getStdin(), 2048); + + if ((is == null) || (os == null)) + throw new IOException("There is a problem with the streams of the underlying channel."); + + init(); + } + + /** + * Create a SFTP v3 client. + * + * @param conn The underlying SSH-2 connection to be used. + * @throws IOException + */ + public SFTPv3Client(Connection conn) throws IOException + { + this(conn, null); + } + + /** + * Set the charset used to convert between Java Unicode Strings and byte encodings + * used by the server for paths and file names. Unfortunately, the SFTP v3 draft + * says NOTHING about such conversions (well, with the exception of error messages + * which have to be in UTF-8). Newer drafts specify to use UTF-8 for file names + * (if I remember correctly). However, a quick test using OpenSSH serving a EXT-3 + * filesystem has shown that UTF-8 seems to be a bad choice for SFTP v3 (tested with + * filenames containing german umlauts). "windows-1252" seems to work better for Europe. + * Luckily, "windows-1252" is the platform default in my case =). + *

+ * If you don't set anything, then the platform default will be used (this is the default + * behavior). + * + * @see #getCharset() + * + * @param charset the name of the charset to be used or null to use the platform's + * default encoding. + * @throws IOException + */ + public void setCharset(String charset) throws IOException + { + if (charset == null) + { + charsetName = charset; + return; + } + + try + { + Charset.forName(charset); + } + catch (Exception e) + { + throw new IOException("This charset is not supported", e); + } + charsetName = charset; + } + + /** + * The currently used charset for filename encoding/decoding. + * + * @see #setCharset(String) + * + * @return The name of the charset (null if the platform's default charset is being used) + */ + public String getCharset() + { + return charsetName; + } + + private final void checkHandleValidAndOpen(SFTPv3FileHandle handle) throws IOException + { + if (handle.client != this) + throw new IOException("The file handle was created with another SFTPv3FileHandle instance."); + + if (handle.isClosed) + throw new IOException("The file handle is closed."); + } + + private final void sendMessage(int type, int requestId, byte[] msg, int off, int len) throws IOException + { + int msglen = len + 1; + + if (type != Packet.SSH_FXP_INIT) + msglen += 4; + + os.write(msglen >> 24); + os.write(msglen >> 16); + os.write(msglen >> 8); + os.write(msglen); + os.write(type); + + if (type != Packet.SSH_FXP_INIT) + { + os.write(requestId >> 24); + os.write(requestId >> 16); + os.write(requestId >> 8); + os.write(requestId); + } + + os.write(msg, off, len); + os.flush(); + } + + private final void sendMessage(int type, int requestId, byte[] msg) throws IOException + { + sendMessage(type, requestId, msg, 0, msg.length); + } + + private final void readBytes(byte[] buff, int pos, int len) throws IOException + { + while (len > 0) + { + int count = is.read(buff, pos, len); + if (count < 0) + throw new IOException("Unexpected end of sftp stream."); + if ((count == 0) || (count > len)) + throw new IOException("Underlying stream implementation is bogus!"); + len -= count; + pos += count; + } + } + + /** + * Read a message and guarantee that the contents is not larger than + * maxlen bytes. + *

+ * Note: receiveMessage(34000) actually means that the message may be up to 34004 + * bytes (the length attribute preceeding the contents is 4 bytes). + * + * @param maxlen + * @return the message contents + * @throws IOException + */ + private final byte[] receiveMessage(int maxlen) throws IOException + { + byte[] msglen = new byte[4]; + + readBytes(msglen, 0, 4); + + int len = (((msglen[0] & 0xff) << 24) | ((msglen[1] & 0xff) << 16) | ((msglen[2] & 0xff) << 8) | (msglen[3] & 0xff)); + + if ((len > maxlen) || (len <= 0)) + throw new IOException("Illegal sftp packet len: " + len); + + byte[] msg = new byte[len]; + + readBytes(msg, 0, len); + + return msg; + } + + private final int generateNextRequestID() + { + synchronized (this) + { + return next_request_id++; + } + } + + private final void closeHandle(byte[] handle) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle, 0, handle.length); + + sendMessage(Packet.SSH_FXP_CLOSE, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + private SFTPv3FileAttributes readAttrs(TypesReader tr) throws IOException + { + /* + * uint32 flags + * uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE + * uint32 uid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID + * uint32 gid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID + * uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS + * uint32 atime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME + * uint32 mtime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME + * uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED + * string extended_type + * string extended_data + * ... more extended data (extended_type - extended_data pairs), + * so that number of pairs equals extended_count + */ + + SFTPv3FileAttributes fa = new SFTPv3FileAttributes(); + + int flags = tr.readUINT32(); + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_SIZE) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_SIZE"); + fa.size = new Long(tr.readUINT64()); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_V3_UIDGID"); + fa.uid = new Integer(tr.readUINT32()); + fa.gid = new Integer(tr.readUINT32()); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_PERMISSIONS"); + fa.permissions = new Integer(tr.readUINT32()); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_V3_ACMODTIME"); + fa.atime = new Long(((long) tr.readUINT32()) & 0xffffffffl); + fa.mtime = new Long(((long) tr.readUINT32()) & 0xffffffffl); + + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_EXTENDED) != 0) + { + int count = tr.readUINT32(); + + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_EXTENDED (" + count + ")"); + + /* Read it anyway to detect corrupt packets */ + + while (count > 0) + { + tr.readByteString(); + tr.readByteString(); + count--; + } + } + + return fa; + } + + /** + * Retrieve the file attributes of an open file. + * + * @param handle a SFTPv3FileHandle handle. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes fstat(SFTPv3FileHandle handle) throws IOException + { + checkHandleValidAndOpen(handle); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + + if (debug != null) + { + debug.println("Sending SSH_FXP_FSTAT..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_FSTAT, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_ATTRS) + { + return readAttrs(tr); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + private SFTPv3FileAttributes statBoth(String path, int statMethod) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_STAT/SSH_FXP_LSTAT..."); + debug.flush(); + } + + sendMessage(statMethod, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_ATTRS) + { + return readAttrs(tr); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + /** + * Retrieve the file attributes of a file. This method + * follows symbolic links on the server. + * + * @see #lstat(String) + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes stat(String path) throws IOException + { + return statBoth(path, Packet.SSH_FXP_STAT); + } + + /** + * Retrieve the file attributes of a file. This method + * does NOT follow symbolic links on the server. + * + * @see #stat(String) + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes lstat(String path) throws IOException + { + return statBoth(path, Packet.SSH_FXP_LSTAT); + } + + /** + * Read the target of a symbolic link. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return The target of the link. + * @throws IOException + */ + public String readLink(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_READLINK..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_READLINK, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (count != 1) + throw new IOException("The server sent an invalid SSH_FXP_NAME packet."); + + return tr.readString(charsetName); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + private void expectStatusOKMessage(int id) throws IOException + { + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != id) + throw new IOException("The server sent an invalid id field."); + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_OK) + return; + + throw new SFTPException(tr.readString(), errorCode); + } + + /** + * Modify the attributes of a file. Used for operations such as changing + * the ownership, permissions or access times, as well as for truncating a file. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be + * made to the attributes of the file. Empty fields will be ignored. + * @throws IOException + */ + public void setstat(String path, SFTPv3FileAttributes attr) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + tw.writeBytes(createAttrs(attr)); + + if (debug != null) + { + debug.println("Sending SSH_FXP_SETSTAT..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_SETSTAT, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Modify the attributes of a file. Used for operations such as changing + * the ownership, permissions or access times, as well as for truncating a file. + * + * @param handle a SFTPv3FileHandle handle + * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be + * made to the attributes of the file. Empty fields will be ignored. + * @throws IOException + */ + public void fsetstat(SFTPv3FileHandle handle, SFTPv3FileAttributes attr) throws IOException + { + checkHandleValidAndOpen(handle); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeBytes(createAttrs(attr)); + + if (debug != null) + { + debug.println("Sending SSH_FXP_FSETSTAT..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_FSETSTAT, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Create a symbolic link on the server. Creates a link "src" that points + * to "target". + * + * @param src See the {@link SFTPv3Client comment} for the class for more details. + * @param target See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void createSymlink(String src, String target) throws IOException + { + int req_id = generateNextRequestID(); + + /* Either I am too stupid to understand the SFTP draft + * or the OpenSSH guys changed the semantics of src and target. + */ + + TypesWriter tw = new TypesWriter(); + tw.writeString(target, charsetName); + tw.writeString(src, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_SYMLINK..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_SYMLINK, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Have the server canonicalize any given path name to an absolute path. + * This is useful for converting path names containing ".." components or + * relative pathnames without a leading slash into absolute paths. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return An absolute path. + * @throws IOException + */ + public String canonicalPath(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_REALPATH..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_REALPATH, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (count != 1) + throw new IOException("The server sent an invalid SSH_FXP_NAME packet."); + + return tr.readString(charsetName); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + private final Vector scanDirectory(byte[] handle) throws IOException + { + Vector files = new Vector(); + + while (true) + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle, 0, handle.length); + + if (debug != null) + { + debug.println("Sending SSH_FXP_READDIR..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_READDIR, req_id, tw.getBytes()); + + /* Some servers send here a packet with size > 34000 */ + /* To whom it may concern: please learn to read the specs. */ + + byte[] resp = receiveMessage(65536); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (debug != null) + debug.println("Parsing " + count + " name entries..."); + + while (count > 0) + { + SFTPv3DirectoryEntry dirEnt = new SFTPv3DirectoryEntry(); + + dirEnt.filename = tr.readString(charsetName); + dirEnt.longEntry = tr.readString(charsetName); + + dirEnt.attributes = readAttrs(tr); + files.addElement(dirEnt); + + if (debug != null) + debug.println("File: '" + dirEnt.filename + "'"); + count--; + } + continue; + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_EOF) + return files; + + throw new SFTPException(tr.readString(), errorCode); + } + } + + private final byte[] openDirectory(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_OPENDIR..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_OPENDIR, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_HANDLE) + { + if (debug != null) + { + debug.println("Got SSH_FXP_HANDLE."); + debug.flush(); + } + + byte[] handle = tr.readByteString(); + return handle; + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + + private final String expandString(byte[] b, int off, int len) + { + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < len; i++) + { + int c = b[off + i] & 0xff; + + if ((c >= 32) && (c <= 126)) + { + sb.append((char) c); + } + else + { + sb.append("{0x" + Integer.toHexString(c) + "}"); + } + } + + return sb.toString(); + } + + private void init() throws IOException + { + /* Send SSH_FXP_INIT (version 3) */ + + final int client_version = 3; + + if (debug != null) + debug.println("Sending SSH_FXP_INIT (" + client_version + ")..."); + + TypesWriter tw = new TypesWriter(); + tw.writeUINT32(client_version); + sendMessage(Packet.SSH_FXP_INIT, 0, tw.getBytes()); + + /* Receive SSH_FXP_VERSION */ + + if (debug != null) + debug.println("Waiting for SSH_FXP_VERSION..."); + + TypesReader tr = new TypesReader(receiveMessage(34000)); /* Should be enough for any reasonable server */ + + int type = tr.readByte(); + + if (type != Packet.SSH_FXP_VERSION) + { + throw new IOException("The server did not send a SSH_FXP_VERSION packet (got " + type + ")"); + } + + protocol_version = tr.readUINT32(); + + if (debug != null) + debug.println("SSH_FXP_VERSION: protocol_version = " + protocol_version); + + if (protocol_version != 3) + throw new IOException("Server version " + protocol_version + " is currently not supported"); + + /* Read and save extensions (if any) for later use */ + + while (tr.remain() != 0) + { + String name = tr.readString(); + byte[] value = tr.readByteString(); + server_extensions.put(name, value); + + if (debug != null) + debug.println("SSH_FXP_VERSION: extension: " + name + " = '" + expandString(value, 0, value.length) + + "'"); + } + } + + /** + * Returns the negotiated SFTP protocol version between the client and the server. + * + * @return SFTP protocol version, i.e., "3". + * + */ + public int getProtocolVersion() + { + return protocol_version; + } + + /** + * Close this SFTP session. NEVER forget to call this method to free up + * resources - even if you got an exception from one of the other methods. + * Sometimes these other methods may throw an exception, saying that the + * underlying channel is closed (this can happen, e.g., if the other server + * sent a close message.) However, as long as you have not called the + * close() method, you are likely wasting resources. + * + */ + public void close() + { + sess.close(); + } + + /** + * List the contents of a directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @return A Vector containing {@link SFTPv3DirectoryEntry} objects. + * @throws IOException + */ + public Vector ls(String dirName) throws IOException + { + byte[] handle = openDirectory(dirName); + Vector result = scanDirectory(handle); + closeHandle(handle); + return result; + } + + /** + * Create a new directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @param posixPermissions the permissions for this directory, e.g., "0700" (remember that + * this is octal noation). The server will likely apply a umask. + * + * @throws IOException + */ + public void mkdir(String dirName, int posixPermissions) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(dirName, charsetName); + tw.writeUINT32(AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS); + tw.writeUINT32(posixPermissions); + + sendMessage(Packet.SSH_FXP_MKDIR, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Remove a file. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void rm(String fileName) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(fileName, charsetName); + + sendMessage(Packet.SSH_FXP_REMOVE, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Remove an empty directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void rmdir(String dirName) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(dirName, charsetName); + + sendMessage(Packet.SSH_FXP_RMDIR, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Move a file or directory. + * + * @param oldPath See the {@link SFTPv3Client comment} for the class for more details. + * @param newPath See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void mv(String oldPath, String newPath) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(oldPath, charsetName); + tw.writeString(newPath, charsetName); + + sendMessage(Packet.SSH_FXP_RENAME, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Open a file for reading. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRO(String fileName) throws IOException + { + return openFile(fileName, 0x00000001, null); // SSH_FXF_READ + } + + /** + * Open a file for reading and writing. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRW(String fileName) throws IOException + { + return openFile(fileName, 0x00000003, null); // SSH_FXF_READ | SSH_FXF_WRITE + } + + // Append is broken (already in the specification, because there is no way to + // send a write operation (what offset to use??)) + // public SFTPv3FileHandle openFileRWAppend(String fileName) throws IOException + // { + // return openFile(fileName, 0x00000007, null); // SSH_FXF_READ | SSH_FXF_WRITE | SSH_FXF_APPEND + // } + + /** + * Create a file and open it for reading and writing. + * Same as {@link #createFile(String, SFTPv3FileAttributes) createFile(fileName, null)}. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFile(String fileName) throws IOException + { + return createFile(fileName, null); + } + + /** + * Create a file and open it for reading and writing. + * You can specify the default attributes of the file (the server may or may + * not respect your wishes). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @param attr may be null to use server defaults. Probably only + * the uid, gid and permissions + * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle} + * structure make sense. You need only to set those fields where you want + * to override the server's defaults. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFile(String fileName, SFTPv3FileAttributes attr) throws IOException + { + return openFile(fileName, 0x00000008 | 0x00000003, attr); // SSH_FXF_CREAT | SSH_FXF_READ | SSH_FXF_WRITE + } + + /** + * Create a file (truncate it if it already exists) and open it for reading and writing. + * Same as {@link #createFileTruncate(String, SFTPv3FileAttributes) createFileTruncate(fileName, null)}. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFileTruncate(String fileName) throws IOException + { + return createFileTruncate(fileName, null); + } + + /** + * reate a file (truncate it if it already exists) and open it for reading and writing. + * You can specify the default attributes of the file (the server may or may + * not respect your wishes). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @param attr may be null to use server defaults. Probably only + * the uid, gid and permissions + * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle} + * structure make sense. You need only to set those fields where you want + * to override the server's defaults. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFileTruncate(String fileName, SFTPv3FileAttributes attr) throws IOException + { + return openFile(fileName, 0x00000018 | 0x00000003, attr); // SSH_FXF_CREAT | SSH_FXF_TRUNC | SSH_FXF_READ | SSH_FXF_WRITE + } + + private byte[] createAttrs(SFTPv3FileAttributes attr) + { + TypesWriter tw = new TypesWriter(); + + int attrFlags = 0; + + if (attr == null) + { + tw.writeUINT32(0); + } + else + { + if (attr.size != null) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_SIZE; + + if ((attr.uid != null) && (attr.gid != null)) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID; + + if (attr.permissions != null) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS; + + if ((attr.atime != null) && (attr.mtime != null)) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME; + + tw.writeUINT32(attrFlags); + + if (attr.size != null) + tw.writeUINT64(attr.size.longValue()); + + if ((attr.uid != null) && (attr.gid != null)) + { + tw.writeUINT32(attr.uid.intValue()); + tw.writeUINT32(attr.gid.intValue()); + } + + if (attr.permissions != null) + tw.writeUINT32(attr.permissions.intValue()); + + if ((attr.atime != null) && (attr.mtime != null)) + { + tw.writeUINT32(attr.atime.intValue()); + tw.writeUINT32(attr.mtime.intValue()); + } + } + + return tw.getBytes(); + } + + private SFTPv3FileHandle openFile(String fileName, int flags, SFTPv3FileAttributes attr) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(fileName, charsetName); + tw.writeUINT32(flags); + tw.writeBytes(createAttrs(attr)); + + if (debug != null) + { + debug.println("Sending SSH_FXP_OPEN..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_OPEN, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_HANDLE) + { + if (debug != null) + { + debug.println("Got SSH_FXP_HANDLE."); + debug.flush(); + } + + return new SFTPv3FileHandle(this, tr.readByteString()); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + + /** + * Read bytes from a file. No more than 32768 bytes may be read at once. + * Be aware that the semantics of read() are different than for Java streams. + *

+ *

    + *
  • The server will read as many bytes as it can from the file (up to len), + * and return them.
  • + *
  • If EOF is encountered before reading any data, -1 is returned.
  • + *
  • If an error occurs, an exception is thrown.
  • + *
  • For normal disk files, it is guaranteed that the server will return the specified + * number of bytes, or up to end of file. For, e.g., device files this may return + * fewer bytes than requested.
  • + *
+ * + * @param handle a SFTPv3FileHandle handle + * @param fileOffset offset (in bytes) in the file + * @param dst the destination byte array + * @param dstoff offset in the destination byte array + * @param len how many bytes to read, 0 < len <= 32768 bytes + * @return the number of bytes that could be read, may be less than requested if + * the end of the file is reached, -1 is returned in case of EOF + * @throws IOException + */ + public int read(SFTPv3FileHandle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException + { + checkHandleValidAndOpen(handle); + + if ((len > 32768) || (len <= 0)) + throw new IllegalArgumentException("invalid len argument"); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeUINT64(fileOffset); + tw.writeUINT32(len); + + if (debug != null) + { + debug.println("Sending SSH_FXP_READ..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_READ, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_DATA) + { + if (debug != null) + { + debug.println("Got SSH_FXP_DATA..."); + debug.flush(); + } + + int readLen = tr.readUINT32(); + + if ((readLen < 0) || (readLen > len)) + throw new IOException("The server sent an invalid length field."); + + tr.readBytes(dst, dstoff, readLen); + + return readLen; + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_EOF) + { + if (debug != null) + { + debug.println("Got SSH_FX_EOF."); + debug.flush(); + } + + return -1; + } + + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + + /** + * Write bytes to a file. If len > 32768, then the write operation will + * be split into multiple writes. + * + * @param handle a SFTPv3FileHandle handle. + * @param fileOffset offset (in bytes) in the file. + * @param src the source byte array. + * @param srcoff offset in the source byte array. + * @param len how many bytes to write. + * @throws IOException + */ + public void write(SFTPv3FileHandle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException + { + checkHandleValidAndOpen(handle); + + while (len > 0) + { + int writeRequestLen = len; + + if (writeRequestLen > 32768) + writeRequestLen = 32768; + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeUINT64(fileOffset); + tw.writeString(src, srcoff, writeRequestLen); + + if (debug != null) + { + debug.println("Sending SSH_FXP_WRITE..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_WRITE, req_id, tw.getBytes()); + + fileOffset += writeRequestLen; + + srcoff += writeRequestLen; + len -= writeRequestLen; + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_OK) + continue; + + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + } + + /** + * Close a file. + * + * @param handle a SFTPv3FileHandle handle + * @throws IOException + */ + public void closeFile(SFTPv3FileHandle handle) throws IOException + { + if (handle == null) + throw new IllegalArgumentException("the handle argument may not be null"); + + try + { + if (!handle.isClosed) + { + closeHandle(handle.fileHandle); + } + } + finally + { + handle.isClosed = true; + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java new file mode 100644 index 0000000000..547c7d2e0d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java @@ -0,0 +1,38 @@ + +package com.trilead.ssh2; + +/** + * A SFTPv3DirectoryEntry as returned by {@link SFTPv3Client#ls(String)}. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3DirectoryEntry.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class SFTPv3DirectoryEntry +{ + /** + * A relative name within the directory, without any path components. + */ + public String filename; + + /** + * An expanded format for the file name, similar to what is returned by + * "ls -l" on Un*x systems. + *

+ * The format of this field is unspecified by the SFTP v3 protocol. + * It MUST be suitable for use in the output of a directory listing + * command (in fact, the recommended operation for a directory listing + * command is to simply display this data). However, clients SHOULD NOT + * attempt to parse the longname field for file attributes; they SHOULD + * use the attrs field instead. + *

+ * The recommended format for the longname field is as follows:
+ * -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer + */ + public String longEntry; + + /** + * The attributes of this entry. + */ + public SFTPv3FileAttributes attributes; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java new file mode 100644 index 0000000000..8fc1cc5bf7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java @@ -0,0 +1,145 @@ + +package com.trilead.ssh2; + +/** + * A SFTPv3FileAttributes object represents detail information + * about a file on the server. Not all fields may/must be present. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3FileAttributes.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class SFTPv3FileAttributes +{ + /** + * The SIZE attribute. NULL if not present. + */ + public Long size = null; + + /** + * The UID attribute. NULL if not present. + */ + public Integer uid = null; + + /** + * The GID attribute. NULL if not present. + */ + public Integer gid = null; + + /** + * The POSIX permissions. NULL if not present. + *

+ * Here is a list: + *

+ *

Note: these numbers are all OCTAL.
+	 *
+	 *  S_IFMT     0170000   bitmask for the file type bitfields
+	 *  S_IFSOCK   0140000   socket
+	 *  S_IFLNK    0120000   symbolic link
+	 *  S_IFREG    0100000   regular file
+	 *  S_IFBLK    0060000   block device
+	 *  S_IFDIR    0040000   directory
+	 *  S_IFCHR    0020000   character device
+	 *  S_IFIFO    0010000   fifo
+	 *  S_ISUID    0004000   set UID bit
+	 *  S_ISGID    0002000   set GID bit
+	 *  S_ISVTX    0001000   sticky bit
+	 *
+	 *  S_IRWXU    00700     mask for file owner permissions
+	 *  S_IRUSR    00400     owner has read permission
+	 *  S_IWUSR    00200     owner has write permission
+	 *  S_IXUSR    00100     owner has execute permission
+	 *  S_IRWXG    00070     mask for group permissions
+	 *  S_IRGRP    00040     group has read permission
+	 *  S_IWGRP    00020     group has write permission
+	 *  S_IXGRP    00010     group has execute permission
+	 *  S_IRWXO    00007     mask for permissions for others (not in group)
+	 *  S_IROTH    00004     others have read permission
+	 *  S_IWOTH    00002     others have write permisson
+	 *  S_IXOTH    00001     others have execute permission
+	 * 
+ */ + public Integer permissions = null; + + /** + * The ATIME attribute. Represented as seconds from Jan 1, 1970 in UTC. + * NULL if not present. + */ + public Long atime = null; + + /** + * The MTIME attribute. Represented as seconds from Jan 1, 1970 in UTC. + * NULL if not present. + */ + public Long mtime = null; + + /** + * Checks if this entry is a directory. + * + * @return Returns true if permissions are available and they indicate + * that this entry represents a directory. + */ + public boolean isDirectory() + { + if (permissions == null) + return false; + + return ((permissions.intValue() & 0040000) != 0); + } + + /** + * Checks if this entry is a regular file. + * + * @return Returns true if permissions are available and they indicate + * that this entry represents a regular file. + */ + public boolean isRegularFile() + { + if (permissions == null) + return false; + + return ((permissions.intValue() & 0100000) != 0); + } + + /** + * Checks if this entry is a a symlink. + * + * @return Returns true if permissions are available and they indicate + * that this entry represents a symlink. + */ + public boolean isSymlink() + { + if (permissions == null) + return false; + + return ((permissions.intValue() & 0120000) != 0); + } + + /** + * Turn the POSIX permissions into a 7 digit octal representation. + * Note: the returned value is first masked with 0177777. + * + * @return NULL if permissions are not available. + */ + public String getOctalPermissions() + { + if (permissions == null) + return null; + + String res = Integer.toString(permissions.intValue() & 0177777, 8); + + StringBuffer sb = new StringBuffer(); + + int leadingZeros = 7 - res.length(); + + while (leadingZeros > 0) + { + sb.append('0'); + leadingZeros--; + } + + sb.append(res); + + return sb.toString(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java new file mode 100644 index 0000000000..817b9cf534 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java @@ -0,0 +1,45 @@ + +package com.trilead.ssh2; + +/** + * A SFTPv3FileHandle. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3FileHandle.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class SFTPv3FileHandle +{ + final SFTPv3Client client; + final byte[] fileHandle; + boolean isClosed = false; + + /* The constructor is NOT public */ + + SFTPv3FileHandle(SFTPv3Client client, byte[] h) + { + this.client = client; + this.fileHandle = h; + } + + /** + * Get the SFTPv3Client instance which created this handle. + * + * @return A SFTPv3Client instance. + */ + public SFTPv3Client getClient() + { + return client; + } + + /** + * Check if this handle was closed with the {@link SFTPv3Client#closeFile(SFTPv3FileHandle)} method + * of the SFTPv3Client instance which created the handle. + * + * @return if the handle is closed. + */ + public boolean isClosed() + { + return isClosed; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java new file mode 100644 index 0000000000..0cdfb8401d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java @@ -0,0 +1,31 @@ + +package com.trilead.ssh2; + +/** + * A callback interface used to implement a client specific method of checking + * server host keys. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ServerHostKeyVerifier.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface ServerHostKeyVerifier +{ + /** + * The actual verifier method, it will be called by the key exchange code + * on EVERY key exchange - this can happen several times during the lifetime + * of a connection. + *

+ * Note: SSH-2 servers are allowed to change their hostkey at ANY time. + * + * @param hostname the hostname used to create the {@link Connection} object + * @param port the remote TCP port + * @param serverHostKeyAlgorithm the public key algorithm (ssh-rsa or ssh-dss) + * @param serverHostKey the server's public key blob + * @return if the client wants to accept the server's host key - if not, the + * connection will be closed. + * @throws Exception Will be wrapped with an IOException, extended version of returning false =) + */ + boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) + throws Exception; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Session.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Session.java new file mode 100644 index 0000000000..29c96e422f --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/Session.java @@ -0,0 +1,529 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; + +import com.trilead.ssh2.channel.Channel; +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.X11ServerData; + + +/** + * A Session is a remote execution of a program. "Program" means + * in this context either a shell, an application or a system command. The + * program may or may not have a tty. Only one single program can be started on + * a session. However, multiple sessions can be active simultaneously. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Session.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $ + */ +public class Session implements AutoCloseable +{ + ChannelManager cm; + Channel cn; + + boolean flag_pty_requested = false; + boolean flag_x11_requested = false; + boolean flag_execution_started = false; + boolean flag_closed = false; + + String x11FakeCookie = null; + + final SecureRandom rnd; + + Session(ChannelManager cm, SecureRandom rnd) throws IOException + { + this.cm = cm; + this.cn = cm.openSessionChannel(); + this.rnd = rnd; + } + + /** + * Basically just a wrapper for lazy people - identical to calling + * requestPTY("dumb", 0, 0, 0, 0, null). + * + * @throws IOException + */ + public void requestDumbPTY() throws IOException + { + requestPTY("dumb", 0, 0, 0, 0, null); + } + + /** + * Basically just another wrapper for lazy people - identical to calling + * requestPTY(term, 0, 0, 0, 0, null). + * + * @throws IOException + */ + public void requestPTY(String term) throws IOException + { + requestPTY(term, 0, 0, 0, 0, null); + } + + /** + * Allocate a pseudo-terminal for this session. + *

+ * This method may only be called before a program or shell is started in + * this session. + *

+ * Different aspects can be specified: + *

+ *

    + *
  • The TERM environment variable value (e.g., vt100)
  • + *
  • The terminal's dimensions.
  • + *
  • The encoded terminal modes.
  • + *
+ * Zero dimension parameters are ignored. The character/row dimensions + * override the pixel dimensions (when nonzero). Pixel dimensions refer to + * the drawable area of the window. The dimension parameters are only + * informational. The encoding of terminal modes (parameter + * terminal_modes) is described in RFC4254. + * + * @param term + * The TERM environment variable value (e.g., vt100) + * @param term_width_characters + * terminal width, characters (e.g., 80) + * @param term_height_characters + * terminal height, rows (e.g., 24) + * @param term_width_pixels + * terminal width, pixels (e.g., 640) + * @param term_height_pixels + * terminal height, pixels (e.g., 480) + * @param terminal_modes + * encoded terminal modes (may be null) + * @throws IOException + */ + public void requestPTY(String term, int term_width_characters, int term_height_characters, int term_width_pixels, + int term_height_pixels, byte[] terminal_modes) throws IOException + { + if (term == null) + throw new IllegalArgumentException("TERM cannot be null."); + + if ((terminal_modes != null) && (terminal_modes.length > 0)) + { + if (terminal_modes[terminal_modes.length - 1] != 0) + throw new IOException("Illegal terminal modes description, does not end in zero byte"); + } + else + terminal_modes = new byte[] { 0 }; + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_pty_requested) + throw new IOException("A PTY was already requested."); + + if (flag_execution_started) + throw new IOException( + "Cannot request PTY at this stage anymore, a remote execution has already started."); + + flag_pty_requested = true; + } + + cm.requestPTY(cn, term, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels, + terminal_modes); + } + + /** + * Inform other side of connection that our PTY has resized. + *

+ * Zero dimension parameters are ignored. The character/row dimensions + * override the pixel dimensions (when nonzero). Pixel dimensions refer to + * the drawable area of the window. The dimension parameters are only + * informational. + * + * @param term_width_characters + * terminal width, characters (e.g., 80) + * @param term_height_characters + * terminal height, rows (e.g., 24) + * @param term_width_pixels + * terminal width, pixels (e.g., 640) + * @param term_height_pixels + * terminal height, pixels (e.g., 480) + * @throws IOException + */ + public void resizePTY(int term_width_characters, int term_height_characters, int term_width_pixels, + int term_height_pixels) throws IOException { + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + cm.resizePTY(cn, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels); + } + + /** + * Request X11 forwarding for the current session. + *

+ * You have to supply the name and port of your X-server. + *

+ * This method may only be called before a program or shell is started in + * this session. + * + * @param hostname the hostname of the real (target) X11 server (e.g., 127.0.0.1) + * @param port the port of the real (target) X11 server (e.g., 6010) + * @param cookie if non-null, then present this cookie to the real X11 server + * @param singleConnection if true, then the server is instructed to only forward one single + * connection, no more connections shall be forwarded after first, or after the session + * channel has been closed + * @throws IOException + */ + public void requestX11Forwarding(String hostname, int port, byte[] cookie, boolean singleConnection) + throws IOException + { + if (hostname == null) + throw new IllegalArgumentException("hostname argument may not be null"); + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_x11_requested) + throw new IOException("X11 forwarding was already requested."); + + if (flag_execution_started) + throw new IOException( + "Cannot request X11 forwarding at this stage anymore, a remote execution has already started."); + + flag_x11_requested = true; + } + + /* X11ServerData - used to store data about the target X11 server */ + + X11ServerData x11data = new X11ServerData(); + + x11data.hostname = hostname; + x11data.port = port; + x11data.x11_magic_cookie = cookie; /* if non-null, then present this cookie to the real X11 server */ + + /* Generate fake cookie - this one is used between remote clients and our proxy */ + + byte[] fakeCookie = new byte[16]; + String hexEncodedFakeCookie; + + /* Make sure that this fake cookie is unique for this connection */ + + while (true) + { + rnd.nextBytes(fakeCookie); + + /* Generate also hex representation of fake cookie */ + + StringBuffer tmp = new StringBuffer(32); + for (int i = 0; i < fakeCookie.length; i++) + { + String digit2 = Integer.toHexString(fakeCookie[i] & 0xff); + tmp.append((digit2.length() == 2) ? digit2 : "0" + digit2); + } + hexEncodedFakeCookie = tmp.toString(); + + /* Well, yes, chances are low, but we want to be on the safe side */ + + if (cm.checkX11Cookie(hexEncodedFakeCookie) == null) + break; + } + + /* Ask for X11 forwarding */ + + cm.requestX11(cn, singleConnection, "MIT-MAGIC-COOKIE-1", hexEncodedFakeCookie, 0); + + /* OK, that went fine, get ready to accept X11 connections... */ + /* ... but only if the user has not called close() in the meantime =) */ + + synchronized (this) + { + if (!flag_closed) + { + this.x11FakeCookie = hexEncodedFakeCookie; + cm.registerX11Cookie(hexEncodedFakeCookie, x11data); + } + } + + /* Now it is safe to start remote X11 programs */ + } + + /** + * Execute a command on the remote machine. + * + * @param cmd + * The command to execute on the remote host. + * @throws IOException + */ + public void execCommand(String cmd) throws IOException + { + if (cmd == null) + throw new IllegalArgumentException("cmd argument may not be null"); + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_execution_started) + throw new IOException("A remote execution has already started."); + + flag_execution_started = true; + } + + cm.requestExecCommand(cn, cmd); + } + + /** + * Start a shell on the remote machine. + * + * @throws IOException + */ + public void startShell() throws IOException + { + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_execution_started) + throw new IOException("A remote execution has already started."); + + flag_execution_started = true; + } + + cm.requestShell(cn); + } + + /** + * Start a subsystem on the remote machine. + * Unless you know what you are doing, you will never need this. + * + * @param name the name of the subsystem. + * @throws IOException + */ + public void startSubSystem(String name) throws IOException + { + if (name == null) + throw new IllegalArgumentException("name argument may not be null"); + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_execution_started) + throw new IOException("A remote execution has already started."); + + flag_execution_started = true; + } + + cm.requestSubSystem(cn, name); + } + + /** + * This method can be used to perform end-to-end session (i.e., SSH channel) + * testing. It sends a 'ping' message to the server and waits for the 'pong' + * from the server. + *

+ * Implementation details: this method sends a SSH_MSG_CHANNEL_REQUEST request + * ('trilead-ping') to the server and waits for the SSH_MSG_CHANNEL_FAILURE reply + * packet. + * + * @throws IOException in case of any problem or when the session is closed + */ + public void ping() throws IOException + { + synchronized (this) + { + /* + * The following is just a nicer error, we would catch it anyway + * later in the channel code + */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + cm.requestChannelTrileadPing(cn); + } + + /** + * Request authentication agent forwarding. + * @param agent object that implements the callbacks + * + * @throws IOException in case of any problem or when the session is closed + */ + public synchronized boolean requestAuthAgentForwarding(AuthAgentCallback agent) throws IOException + { + synchronized (this) + { + /* + * The following is just a nicer error, we would catch it anyway + * later in the channel code + */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + return cm.requestChannelAgentForwarding(cn, agent); + } + + public InputStream getStdout() + { + return cn.getStdoutStream(); + } + + public InputStream getStderr() + { + return cn.getStderrStream(); + } + + public OutputStream getStdin() + { + return cn.getStdinStream(); + } + + /** + * This method blocks until there is more data available on either the + * stdout or stderr InputStream of this Session. Very useful + * if you do not want to use two parallel threads for reading from the two + * InputStreams. One can also specify a timeout. NOTE: do NOT call this + * method if you use concurrent threads that operate on either of the two + * InputStreams of this Session (otherwise this method may + * block, even though more data is available). + * + * @param timeout + * The (non-negative) timeout in ms. 0 means no + * timeout, the call may block forever. + * @return + *

    + *
  • 0 if no more data will arrive.
  • + *
  • 1 if more data is available.
  • + *
  • -1 if a timeout occurred.
  • + *
+ * + * @deprecated This method has been replaced with a much more powerful wait-for-condition + * interface and therefore acts only as a wrapper. + * + */ + @Deprecated + public int waitUntilDataAvailable(long timeout) { + if (timeout < 0) + throw new IllegalArgumentException("timeout must not be negative!"); + + int conditions = cm.waitForCondition(cn, timeout, ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA + | ChannelCondition.EOF); + + if ((conditions & ChannelCondition.TIMEOUT) != 0) + return -1; + + if ((conditions & (ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA)) != 0) + return 1; + + /* Here we do not need to check separately for CLOSED, since CLOSED implies EOF */ + + if ((conditions & ChannelCondition.EOF) != 0) + return 0; + + throw new IllegalStateException("Unexpected condition result (" + conditions + ")"); + } + + /** + * This method blocks until certain conditions hold true on the underlying SSH-2 channel. + *

+ * This method returns as soon as one of the following happens: + *

    + *
  • at least of the specified conditions (see {@link ChannelCondition}) holds true
  • + *
  • timeout > 0 and a timeout occured (TIMEOUT will be set in result conditions)
  • + *
  • the underlying channel was closed (CLOSED will be set in result conditions)
  • + *
+ *

+ * In any case, the result value contains ALL current conditions, which may be more + * than the specified condition set (i.e., never use the "==" operator to test for conditions + * in the bitmask, see also comments in {@link ChannelCondition}). + *

+ * Note: do NOT call this method if you want to wait for STDOUT_DATA or STDERR_DATA and + * there are concurrent threads (e.g., StreamGobblers) that operate on either of the two + * InputStreams of this Session (otherwise this method may + * block, even though more data is available in the StreamGobblers). + * + * @param condition_set a bitmask based on {@link ChannelCondition} values + * @param timeout non-negative timeout in ms, 0 means no timeout + * @return all bitmask specifying all current conditions that are true + */ + + public int waitForCondition(int condition_set, long timeout) + { + if (timeout < 0) + throw new IllegalArgumentException("timeout must be non-negative!"); + + return cm.waitForCondition(cn, timeout, condition_set); + } + + /** + * Get the exit code/status from the remote command - if available. Be + * careful - not all server implementations return this value. It is + * generally a good idea to call this method only when all data from the + * remote side has been consumed (see also the WaitForCondition method). + * + * @return An Integer holding the exit code, or + * null if no exit code is (yet) available. + */ + public Integer getExitStatus() + { + return cn.getExitStatus(); + } + + /** + * Get the name of the signal by which the process on the remote side was + * stopped - if available and applicable. Be careful - not all server + * implementations return this value. + * + * @return An String holding the name of the signal, or + * null if the process exited normally or is still + * running (or if the server forgot to send this information). + */ + public String getExitSignal() + { + return cn.getExitSignal(); + } + + /** + * Close this session. NEVER forget to call this method to free up resources - + * even if you got an exception from one of the other methods (or when + * getting an Exception on the Input- or OutputStreams). Sometimes these other + * methods may throw an exception, saying that the underlying channel is + * closed (this can happen, e.g., if the other server sent a close message.) + * However, as long as you have not called the close() + * method, you may be wasting (local) resources. + * + */ + public void close() + { + synchronized (this) + { + if (flag_closed) + return; + + flag_closed = true; + + if (x11FakeCookie != null) + cm.unRegisterX11Cookie(x11FakeCookie, true); + + try + { + cm.closeChannel(cn, "Closed due to user request", true); + } + catch (IOException ignored) + { + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/StreamGobbler.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/StreamGobbler.java new file mode 100644 index 0000000000..3c9572768b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/StreamGobbler.java @@ -0,0 +1,229 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A StreamGobbler is an InputStream that uses an internal worker + * thread to constantly consume input from another InputStream. It uses a buffer + * to store the consumed data. The buffer size is automatically adjusted, if needed. + *

+ * This class is sometimes very convenient - if you wrap a session's STDOUT and STDERR + * InputStreams with instances of this class, then you don't have to bother about + * the shared window of STDOUT and STDERR in the low level SSH-2 protocol, + * since all arriving data will be immediatelly consumed by the worker threads. + * Also, as a side effect, the streams will be buffered (e.g., single byte + * read() operations are faster). + *

+ * Other SSH for Java libraries include this functionality by default in + * their STDOUT and STDERR InputStream implementations, however, please be aware + * that this approach has also a downside: + *

+ * If you do not call the StreamGobbler's read() method often enough + * and the peer is constantly sending huge amounts of data, then you will sooner or later + * encounter a low memory situation due to the aggregated data (well, it also depends on the Java heap size). + * Joe Average will like this class anyway - a paranoid programmer would never use such an approach. + *

+ * The term "StreamGobbler" was taken from an article called "When Runtime.exec() won't", + * see http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: StreamGobbler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class StreamGobbler extends InputStream +{ + class GobblerThread extends Thread + { + public void run() + { + byte[] buff = new byte[8192]; + + while (true) + { + try + { + int avail = is.read(buff); + + synchronized (synchronizer) + { + if (avail <= 0) + { + isEOF = true; + synchronizer.notifyAll(); + break; + } + + int space_available = buffer.length - write_pos; + + if (space_available < avail) + { + /* compact/resize buffer */ + + int unread_size = write_pos - read_pos; + int need_space = unread_size + avail; + + byte[] new_buffer = buffer; + + if (need_space > buffer.length) + { + int inc = need_space / 3; + inc = (inc < 256) ? 256 : inc; + inc = (inc > 8192) ? 8192 : inc; + new_buffer = new byte[need_space + inc]; + } + + if (unread_size > 0) + System.arraycopy(buffer, read_pos, new_buffer, 0, unread_size); + + buffer = new_buffer; + + read_pos = 0; + write_pos = unread_size; + } + + System.arraycopy(buff, 0, buffer, write_pos, avail); + write_pos += avail; + + synchronizer.notifyAll(); + } + } + catch (IOException e) + { + synchronized (synchronizer) + { + exception = e; + synchronizer.notifyAll(); + break; + } + } + } + } + } + + private InputStream is; + private GobblerThread t; + + private Object synchronizer = new Object(); + + private boolean isEOF = false; + private boolean isClosed = false; + private IOException exception = null; + + private byte[] buffer = new byte[2048]; + private int read_pos = 0; + private int write_pos = 0; + + public StreamGobbler(InputStream is) + { + this.is = is; + t = new GobblerThread(); + t.setDaemon(true); + t.start(); + } + + public int read() throws IOException + { + synchronized (synchronizer) + { + if (isClosed) + throw new IOException("This StreamGobbler is closed."); + + while (read_pos == write_pos) + { + if (exception != null) + throw exception; + + if (isEOF) + return -1; + + try + { + synchronizer.wait(); + } + catch (InterruptedException e) + { + } + } + + int b = buffer[read_pos++] & 0xff; + + return b; + } + } + + public int available() throws IOException + { + synchronized (synchronizer) + { + if (isClosed) + throw new IOException("This StreamGobbler is closed."); + + return write_pos - read_pos; + } + } + + public int read(byte[] b) throws IOException + { + return read(b, 0, b.length); + } + + public void close() throws IOException + { + synchronized (synchronizer) + { + if (isClosed) + return; + isClosed = true; + isEOF = true; + synchronizer.notifyAll(); + is.close(); + } + } + + public int read(byte[] b, int off, int len) throws IOException + { + if (b == null) + throw new NullPointerException(); + + if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) + throw new IndexOutOfBoundsException(); + + if (len == 0) + return 0; + + synchronized (synchronizer) + { + if (isClosed) + throw new IOException("This StreamGobbler is closed."); + + while (read_pos == write_pos) + { + if (exception != null) + throw exception; + + if (isEOF) + return -1; + + try + { + synchronizer.wait(); + } + catch (InterruptedException e) + { + } + } + + int avail = write_pos - read_pos; + + avail = (avail > len) ? len : avail; + + System.arraycopy(buffer, read_pos, b, off, avail); + + read_pos += avail; + + return avail; + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java new file mode 100644 index 0000000000..47460ca307 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java @@ -0,0 +1,518 @@ + +package com.trilead.ssh2.auth; + +import com.trilead.ssh2.crypto.keys.Ed25519PrivateKey; +import com.trilead.ssh2.crypto.keys.Ed25519PublicKey; +import com.trilead.ssh2.signature.RSASHA256Verify; +import com.trilead.ssh2.signature.RSASHA512Verify; +import java.io.IOException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Set; +import java.util.Vector; + +import com.trilead.ssh2.InteractiveCallback; +import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.packets.PacketServiceAccept; +import com.trilead.ssh2.packets.PacketServiceRequest; +import com.trilead.ssh2.packets.PacketUserauthBanner; +import com.trilead.ssh2.packets.PacketUserauthFailure; +import com.trilead.ssh2.packets.PacketUserauthInfoRequest; +import com.trilead.ssh2.packets.PacketUserauthInfoResponse; +import com.trilead.ssh2.packets.PacketUserauthRequestInteractive; +import com.trilead.ssh2.packets.PacketUserauthRequestNone; +import com.trilead.ssh2.packets.PacketUserauthRequestPassword; +import com.trilead.ssh2.packets.PacketUserauthRequestPublicKey; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.Ed25519Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; +import com.trilead.ssh2.signature.SSHSignature; +import com.trilead.ssh2.transport.MessageHandler; +import com.trilead.ssh2.transport.TransportManager; + + +/** + * AuthenticationManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AuthenticationManager.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class AuthenticationManager implements MessageHandler +{ + TransportManager tm; + + Vector packets = new Vector(); + boolean connectionClosed = false; + + String banner; + + String[] remainingMethods = new String[0]; + boolean isPartialSuccess = false; + + boolean authenticated = false; + boolean initDone = false; + + public AuthenticationManager(TransportManager tm) + { + this.tm = tm; + } + + boolean methodPossible(String methName) + { + if (remainingMethods == null) + return false; + + for (int i = 0; i < remainingMethods.length; i++) + { + if (remainingMethods[i].compareTo(methName) == 0) + return true; + } + return false; + } + + byte[] deQueue() throws IOException + { + synchronized (packets) + { + while (packets.size() == 0) + { + if (connectionClosed) + throw new IOException("The connection is closed.", tm.getReasonClosedCause()); + + try + { + packets.wait(); + } + catch (InterruptedException ign) + { + } + } + /* This sequence works with J2ME */ + byte[] res = (byte[]) packets.firstElement(); + packets.removeElementAt(0); + return res; + } + } + + byte[] getNextMessage() throws IOException + { + while (true) + { + byte[] msg = deQueue(); + + if (msg[0] != Packets.SSH_MSG_USERAUTH_BANNER) + return msg; + + PacketUserauthBanner sb = new PacketUserauthBanner(msg, 0, msg.length); + + banner = sb.getBanner(); + } + } + + public String[] getRemainingMethods(String user) throws IOException + { + initialize(user); + return remainingMethods; + } + + public boolean getPartialSuccess() + { + return isPartialSuccess; + } + + private boolean initialize(String user) throws IOException + { + if (!initDone) + { + tm.registerMessageHandler(this, 0, 255); + + PacketServiceRequest sr = new PacketServiceRequest("ssh-userauth"); + tm.sendMessage(sr.getPayload()); + + PacketUserauthRequestNone urn = new PacketUserauthRequestNone("ssh-connection", user); + tm.sendMessage(urn.getPayload()); + + byte[] msg = getNextMessage(); + new PacketServiceAccept(msg, 0, msg.length); + msg = getNextMessage(); + + initDone = true; + + if (msg[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (msg[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(msg, 0, msg.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + return false; + } + + throw new IOException("Unexpected SSH message (type " + msg[0] + ")"); + } + return authenticated; + } + + public boolean authenticatePublicKey(String user, char[] PEMPrivateKey, String password, SecureRandom rnd) + throws IOException + { + KeyPair pair = PEMDecoder.decode(PEMPrivateKey, password); + + return authenticatePublicKey(user, pair, rnd); + } + + public boolean authenticatePublicKey(String user, KeyPair pair, SecureRandom rnd) + throws IOException + { + return authenticatePublicKey(user, pair, rnd, null); + } + + public boolean authenticatePublicKey(String user, SignatureProxy signatureProxy) + throws IOException + { + return authenticatePublicKey(user, null, null, signatureProxy); + } + + public boolean authenticatePublicKey(String user, KeyPair pair, SecureRandom rnd, SignatureProxy signatureProxy) + throws IOException + { + PrivateKey privateKey = null; + PublicKey publicKey = null; + if (pair != null) + { + privateKey = pair.getPrivate(); + publicKey = pair.getPublic(); + } + if (signatureProxy != null) + { + publicKey = signatureProxy.getPublicKey(); + } + + try + { + initialize(user); + + if (!methodPossible("publickey")) + throw new IOException("Authentication method publickey not supported by the server at this stage."); + + if (publicKey instanceof DSAPublicKey) + { + SSHSignature s = DSASHA1Verify.get(); + byte[] pk_enc = s.encodePublicKey(publicKey); + + byte[] msg = this.generatePublicKeyUserAuthenticationRequest(user, DSASHA1Verify.ID_SSH_DSS, pk_enc); + + byte[] ds_enc; + if (signatureProxy != null) + { + ds_enc = signatureProxy.sign(msg, SignatureProxy.SHA1); + } + else + { + ds_enc = s.generateSignature(msg, privateKey, rnd); + } + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + DSASHA1Verify.ID_SSH_DSS, pk_enc, ds_enc); + tm.sendMessage(ua.getPayload()); + } + else if (publicKey instanceof RSAPublicKey) + { + byte[] pk_enc = RSASHA1Verify.get().encodePublicKey(publicKey); + String pk_algorithm; + + + // Servers support different hash algorithms for RSA keys + // https://tools.ietf.org/html/draft-ietf-curdle-rsa-sha2-12 + Set algsAccepted = tm.getExtensionInfo().getSignatureAlgorithmsAccepted(); + final byte[] rsa_sig_enc; + + if (algsAccepted.contains(RSASHA512Verify.get().getKeyFormat())) + { + SSHSignature s = RSASHA512Verify.get(); + pk_algorithm = s.getKeyFormat(); + byte[] msg = this.generatePublicKeyUserAuthenticationRequest(user, pk_algorithm, pk_enc); + if (signatureProxy != null) + { + rsa_sig_enc = signatureProxy.sign(msg, SignatureProxy.SHA512); + } + else + { + rsa_sig_enc = s.generateSignature(msg, privateKey, rnd); + } + } + else if (algsAccepted.contains(RSASHA256Verify.ID_RSA_SHA_2_256)) + { + pk_algorithm = RSASHA256Verify.ID_RSA_SHA_2_256; + byte[] msg = this.generatePublicKeyUserAuthenticationRequest(user, pk_algorithm, pk_enc); + + if (signatureProxy != null) + { + rsa_sig_enc = signatureProxy.sign(msg, SignatureProxy.SHA256); + } + else + { + rsa_sig_enc = RSASHA256Verify.get().generateSignature(msg, privateKey, rnd); + } + } + else + { + pk_algorithm = "ssh-rsa"; + byte[] msg = this.generatePublicKeyUserAuthenticationRequest(user, pk_algorithm, pk_enc); + if (signatureProxy != null) + { + rsa_sig_enc = signatureProxy.sign(msg, SignatureProxy.SHA1); + } + else + { + // Server always accepts RSA with SHA1 + rsa_sig_enc = RSASHA1Verify.get().generateSignature(msg, privateKey, rnd); + } + } + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + pk_algorithm, pk_enc, rsa_sig_enc); + + tm.sendMessage(ua.getPayload()); + } + else if (publicKey instanceof ECPublicKey) + { + ECPublicKey ecPublicKey = (ECPublicKey) publicKey; + + ECDSASHA2Verify verifier = ECDSASHA2Verify.getVerifierForKey(ecPublicKey); + + final String algo = verifier.getKeyFormat(); + + byte[] pk_enc = verifier.encodePublicKey(ecPublicKey); + + byte[] msg = this.generatePublicKeyUserAuthenticationRequest(user, algo, pk_enc); + + byte[] ec_sig_enc; + if (signatureProxy != null) + { + ec_sig_enc = signatureProxy.sign(msg, ECDSASHA2Verify.getDigestAlgorithmForParams(ecPublicKey)); + } + else + { + ec_sig_enc = verifier.generateSignature(msg, privateKey, rnd); + } + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + algo, pk_enc, ec_sig_enc); + + tm.sendMessage(ua.getPayload()); + } + else if (publicKey instanceof Ed25519PublicKey) + { + final String algo = Ed25519Verify.ED25519_ID; + + byte[] pk_enc = Ed25519Verify.get().encodePublicKey(publicKey); + + byte[] msg = this.generatePublicKeyUserAuthenticationRequest(user, algo, pk_enc); + + byte[] ed_sig_enc; + if (signatureProxy != null) + { + ed_sig_enc = signatureProxy.sign(msg, SignatureProxy.SHA512); + } + else + { + Ed25519PrivateKey pk = (Ed25519PrivateKey) privateKey; + ed_sig_enc = Ed25519Verify.get().generateSignature(msg, pk, rnd); + } + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + algo, pk_enc, ed_sig_enc); + + tm.sendMessage(ua.getPayload()); + } + else + { + throw new IOException("Unknown public key type."); + } + + byte[] ar = getNextMessage(); + + return isAuthenticationSuccessful(ar); + } + catch (IOException e) + { + e.printStackTrace(); + tm.close(e, false); + throw new IOException("Publickey authentication failed.", e); + } + } + + public boolean authenticateNone(String user) throws IOException + { + try + { + initialize(user); + return authenticated; + } + catch (IOException e) + { + tm.close(e, false); + throw new IOException("None authentication failed.", e); + } + } + + public boolean authenticatePassword(String user, String pass) throws IOException + { + try + { + initialize(user); + + if (!methodPossible("password")) + throw new IOException("Authentication method password not supported by the server at this stage."); + + PacketUserauthRequestPassword ua = new PacketUserauthRequestPassword("ssh-connection", user, pass); + tm.sendMessage(ua.getPayload()); + + byte[] ar = getNextMessage(); + + return isAuthenticationSuccessful(ar); + } + catch (IOException e) + { + tm.close(e, false); + throw new IOException("Password authentication failed.", e); + } + } + + public boolean authenticateInteractive(String user, String[] submethods, InteractiveCallback cb) throws IOException + { + try + { + initialize(user); + + if (!methodPossible("keyboard-interactive")) + throw new IOException( + "Authentication method keyboard-interactive not supported by the server at this stage."); + + if (submethods == null) + submethods = new String[0]; + + PacketUserauthRequestInteractive ua = new PacketUserauthRequestInteractive("ssh-connection", user, + submethods); + + tm.sendMessage(ua.getPayload()); + + while (true) + { + byte[] ar = getNextMessage(); + + if (ar[0] == Packets.SSH_MSG_USERAUTH_INFO_REQUEST) + { + PacketUserauthInfoRequest pui = new PacketUserauthInfoRequest(ar, 0, ar.length); + + String[] responses; + + try + { + responses = cb.replyToChallenge(pui.getName(), pui.getInstruction(), pui.getNumPrompts(), pui + .getPrompt(), pui.getEcho()); + } + catch (Exception e) + { + throw new IOException("Exception in callback.", e); + } + + if (responses == null) + throw new IOException("Your callback may not return NULL!"); + + PacketUserauthInfoResponse puir = new PacketUserauthInfoResponse(responses); + tm.sendMessage(puir.getPayload()); + + continue; + } + + return isAuthenticationSuccessful(ar); + } + } + catch (IOException e) + { + tm.close(e, false); + throw new IOException("Keyboard-interactive authentication failed.", e); + } + } + + public void handleMessage(byte[] msg, int msglen) throws IOException + { + synchronized (packets) + { + if (msg == null) + { + connectionClosed = true; + } + else + { + byte[] tmp = new byte[msglen]; + System.arraycopy(msg, 0, tmp, 0, msglen); + packets.addElement(tmp); + } + + packets.notifyAll(); + + if (packets.size() > 5) + { + connectionClosed = true; + throw new IOException("Error, peer is flooding us with authentication packets."); + } + } + } + + private boolean isAuthenticationSuccessful(byte[] ar) throws IOException + { + if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + + return false; + } + + throw new IOException("Unexpected SSH message (type " + ar[0] + ")"); + } + + private byte[] generatePublicKeyUserAuthenticationRequest(String user, String algorithm, byte[] publicKeyEncoded) { + TypesWriter tw = new TypesWriter(); + { + byte[] H = tm.getSessionIdentifier(); + + tw.writeString(H, 0, H.length); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(user); + tw.writeString("ssh-connection"); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString(algorithm); + tw.writeString(publicKeyEncoded, 0, publicKeyEncoded.length); + } + + return tw.getBytes(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/SignatureProxy.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/SignatureProxy.java new file mode 100644 index 0000000000..bdf00bbcfd --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/auth/SignatureProxy.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Jonas Dippel, Michael Perk, Marc Totzke + */ + +package com.trilead.ssh2.auth; + +import java.io.IOException; +import java.security.PublicKey; + +public abstract class SignatureProxy +{ + public static final String SHA1 = "SHA-1"; + public static final String SHA256 = "SHA-256"; + public static final String SHA384 = "SHA-384"; + public static final String SHA512 = "SHA-512"; + + /** + * Holds the public key which belongs to the private key which is used in the signing process. + */ + private PublicKey mPublicKey; + + /** + * Instantiates a new SignatureProxy which needs a public key for the + * later authentication process. + * + * @param publicKey The public key. + * @throws IllegalArgumentException Might be thrown id the public key is invalid. + */ + public SignatureProxy(PublicKey publicKey) + { + if (publicKey == null) + { + throw new IllegalArgumentException("Public key must not be null"); + } + mPublicKey = publicKey; + } + + /** + * This method should sign a given byte array message using the private key. + * + * @param message The message which should be signed. + * @param hashAlgorithm The hashing algorithm which should be used. + * @return The signed message. + * @throws IOException This exception might be thrown during the signing process. + */ + public abstract byte[] sign(byte[] message, String hashAlgorithm) throws IOException; + + public PublicKey getPublicKey() + { + return mPublicKey; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java new file mode 100644 index 0000000000..b7c753a6ed --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java @@ -0,0 +1,605 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.channel; + +import com.trilead.ssh2.signature.RSASHA256Verify; +import com.trilead.ssh2.signature.RSASHA512Verify; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Map; +import java.util.Map.Entry; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.crypto.keys.Ed25519PrivateKey; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.Ed25519Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + +/** + * AuthAgentForwardThread. + * + * @author Kenny Root + * @version $Id$ + */ +public class AuthAgentForwardThread extends Thread implements IChannelWorkerThread +{ + private static final byte[] SSH_AGENT_FAILURE = {0, 0, 0, 1, 5}; // 5 + private static final byte[] SSH_AGENT_SUCCESS = {0, 0, 0, 1, 6}; // 6 + + private static final int SSH2_AGENTC_REQUEST_IDENTITIES = 11; + private static final int SSH2_AGENT_IDENTITIES_ANSWER = 12; + + private static final int SSH2_AGENTC_SIGN_REQUEST = 13; + private static final int SSH2_AGENT_SIGN_RESPONSE = 14; + + private static final int SSH2_AGENTC_ADD_IDENTITY = 17; + private static final int SSH2_AGENTC_REMOVE_IDENTITY = 18; + private static final int SSH2_AGENTC_REMOVE_ALL_IDENTITIES = 19; + +// private static final int SSH_AGENTC_ADD_SMARTCARD_KEY = 20; +// private static final int SSH_AGENTC_REMOVE_SMARTCARD_KEY = 21; + + private static final int SSH_AGENTC_LOCK = 22; + private static final int SSH_AGENTC_UNLOCK = 23; + + private static final int SSH2_AGENTC_ADD_ID_CONSTRAINED = 25; +// private static final int SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED = 26; + + // Constraints for adding keys + private static final int SSH_AGENT_CONSTRAIN_LIFETIME = 1; + private static final int SSH_AGENT_CONSTRAIN_CONFIRM = 2; + + // Flags for signature requests +// private static final int SSH_AGENT_OLD_SIGNATURE = 1; + // https://tools.ietf.org/html/draft-miller-ssh-agent-02#section-7.3 + private static final int SSH_AGENT_RSA_SHA2_256 = 0x02; + private static final int SSH_AGENT_RSA_SHA2_512 = 0x04; + + private static final Logger log = Logger.getLogger(RemoteAcceptThread.class); + + AuthAgentCallback authAgent; + OutputStream os; + InputStream is; + Channel c; + + byte[] buffer = new byte[Channel.CHANNEL_BUFFER_SIZE]; + + public AuthAgentForwardThread(Channel c, AuthAgentCallback authAgent) + { + this.c = c; + this.authAgent = authAgent; + + if (log.isEnabled()) + log.log(20, "AuthAgentForwardThread started"); + } + + @Override + public void run() + { + try + { + c.cm.registerThread(this); + } + catch (IOException e) + { + stopWorking(); + return; + } + + try + { + c.cm.sendOpenConfirmation(c); + + is = c.getStdoutStream(); + os = c.getStdinStream(); + + int totalSize = 4; + int readSoFar = 0; + + while (true) { + int len; + + try + { + len = is.read(buffer, readSoFar, buffer.length - readSoFar); + } + catch (IOException e) + { + stopWorking(); + return; + } + + if (len <= 0) + break; + + readSoFar += len; + + if (readSoFar >= 4) { + TypesReader tr = new TypesReader(buffer, 0, 4); + totalSize = tr.readUINT32() + 4; + } + + if (totalSize == readSoFar) { + TypesReader tr = new TypesReader(buffer, 4, readSoFar - 4); + int messageType = tr.readByte(); + + switch (messageType) { + case SSH2_AGENTC_REQUEST_IDENTITIES: + sendIdentities(); + break; + case SSH2_AGENTC_ADD_IDENTITY: + addIdentity(tr, false); + break; + case SSH2_AGENTC_ADD_ID_CONSTRAINED: + addIdentity(tr, true); + break; + case SSH2_AGENTC_REMOVE_IDENTITY: + removeIdentity(tr); + break; + case SSH2_AGENTC_REMOVE_ALL_IDENTITIES: + removeAllIdentities(tr); + break; + case SSH2_AGENTC_SIGN_REQUEST: + processSignRequest(tr); + break; + case SSH_AGENTC_LOCK: + processLockRequest(tr); + break; + case SSH_AGENTC_UNLOCK: + processUnlockRequest(tr); + break; + default: + os.write(SSH_AGENT_FAILURE); + break; + } + + readSoFar = 0; + } + } + + c.cm.closeChannel(c, "EOF on both streams reached.", true); + } + catch (IOException e) + { + log.log(50, "IOException in agent forwarder: " + e.getMessage()); + + try + { + is.close(); + } + catch (IOException e1) + { + } + + try + { + os.close(); + } + catch (IOException e2) + { + } + + try + { + c.cm.closeChannel(c, "IOException in agent forwarder (" + e.getMessage() + ")", true); + } + catch (IOException e3) + { + } + } + } + + public void stopWorking() { + try + { + /* This will lead to an IOException in the is.read() call */ + is.close(); + } + catch (IOException e) + { + } + } + + /** + * @return whether the agent is locked + */ + private boolean failWhenLocked() throws IOException + { + if (authAgent.isAgentLocked()) { + os.write(SSH_AGENT_FAILURE); + return true; + } else + return false; + } + + private void sendIdentities() throws IOException + { + Map keys = null; + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_IDENTITIES_ANSWER); + int numKeys = 0; + + if (!authAgent.isAgentLocked()) + keys = authAgent.retrieveIdentities(); + + if (keys != null) + numKeys = keys.size(); + + tw.writeUINT32(numKeys); + + if (keys != null) { + for (Entry entry : keys.entrySet()) { + byte[] keyBytes = entry.getValue(); + tw.writeString(keyBytes, 0, keyBytes.length); + tw.writeString(entry.getKey()); + } + } + + sendPacket(tw.getBytes()); + } + + /** + * @param tr + */ + private void addIdentity(TypesReader tr, boolean checkConstraints) { + try + { + if (failWhenLocked()) + return; + + String type = tr.readString(); + + String comment; + String keyType; + KeySpec pubSpec; + KeySpec privSpec; + + if (type.equals("ssh-rsa")) { + keyType = "RSA"; + + BigInteger n = tr.readMPINT(); + BigInteger e = tr.readMPINT(); + BigInteger d = tr.readMPINT(); + BigInteger iqmp = tr.readMPINT(); + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + comment = tr.readString(); + + // Derive the extra values Java needs. + BigInteger dmp1 = d.mod(p.subtract(BigInteger.ONE)); + BigInteger dmq1 = d.mod(q.subtract(BigInteger.ONE)); + + pubSpec = new RSAPublicKeySpec(n, e); + privSpec = new RSAPrivateCrtKeySpec(n, e, d, p, q, dmp1, dmq1, iqmp); + } else if (type.equals(DSASHA1Verify.ID_SSH_DSS)) { + keyType = "DSA"; + + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + BigInteger g = tr.readMPINT(); + BigInteger y = tr.readMPINT(); + BigInteger x = tr.readMPINT(); + comment = tr.readString(); + + pubSpec = new DSAPublicKeySpec(y, p, q, g); + privSpec = new DSAPrivateKeySpec(x, p, q, g); + } else if (type.equals(ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().getKeyFormat())) { + ECDSASHA2Verify verifier = ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get(); + keyType = "EC"; + + String curveName = tr.readString(); + byte[] groupBytes = tr.readByteString(); + BigInteger exponent = tr.readMPINT(); + comment = tr.readString(); + + if (!"nistp256".equals(curveName)) { + log.log(2, "Invalid curve name for ecdsa-sha2-nistp256: " + curveName); + os.write(SSH_AGENT_FAILURE); + return; + } + + ECParameterSpec params = verifier.getParameterSpec(); + ECPoint group = verifier.decodeECPoint(groupBytes); + if (group == null) { + // TODO log error + os.write(SSH_AGENT_FAILURE); + return; + } + + pubSpec = new ECPublicKeySpec(group, params); + privSpec = new ECPrivateKeySpec(exponent, params); + } else { + log.log(2, "Unknown key type: " + type); + os.write(SSH_AGENT_FAILURE); + return; + } + + PublicKey pubKey; + PrivateKey privKey; + try { + KeyFactory kf = KeyFactory.getInstance(keyType); + pubKey = kf.generatePublic(pubSpec); + privKey = kf.generatePrivate(privSpec); + } catch (NoSuchAlgorithmException ex) { + // TODO: log error + os.write(SSH_AGENT_FAILURE); + return; + } catch (InvalidKeySpecException ex) { + // TODO: log error + os.write(SSH_AGENT_FAILURE); + return; + } + + KeyPair pair = new KeyPair(pubKey, privKey); + + boolean confirmUse = false; + int lifetime = 0; + + if (checkConstraints) { + while (tr.remain() > 0) { + int constraint = tr.readByte(); + if (constraint == SSH_AGENT_CONSTRAIN_CONFIRM) + confirmUse = true; + else if (constraint == SSH_AGENT_CONSTRAIN_LIFETIME) + lifetime = tr.readUINT32(); + else { + // Unknown constraint. Bail. + os.write(SSH_AGENT_FAILURE); + return; + } + } + } + + if (authAgent.addIdentity(pair, comment, confirmUse, lifetime)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeIdentity(TypesReader tr) { + try + { + if (failWhenLocked()) + return; + + byte[] publicKey = tr.readByteString(); + if (authAgent.removeIdentity(publicKey)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeAllIdentities(TypesReader tr) { + try + { + if (failWhenLocked()) + return; + + if (authAgent.removeAllIdentities()) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + private void processSignRequest(TypesReader tr) + { + try + { + if (failWhenLocked()) + return; + + byte[] publicKeyBytes = tr.readByteString(); + byte[] challenge = tr.readByteString(); + + int flags = tr.readUINT32(); + + if ((flags & ~SSH_AGENT_RSA_SHA2_512 & ~SSH_AGENT_RSA_SHA2_256) != 0) { + // We don't understand these flags; abort! + log.log(2, "Unrecognized ssh-agent flags: " + flags); + os.write(SSH_AGENT_FAILURE); + return; + } + + KeyPair pair = authAgent.getKeyPair(publicKeyBytes); + + if (pair == null) { + os.write(SSH_AGENT_FAILURE); + return; + } + + byte[] response; + + PrivateKey privKey = pair.getPrivate(); + if (privKey instanceof RSAPrivateKey) { + RSAPrivateKey rsaPrivKey = (RSAPrivateKey) privKey; + if ((flags & SSH_AGENT_RSA_SHA2_512) != 0) { + response = RSASHA512Verify.get().generateSignature(challenge, rsaPrivKey, new SecureRandom()); + } else if ((flags & SSH_AGENT_RSA_SHA2_256) != 0) { + response = RSASHA256Verify.get().generateSignature(challenge, rsaPrivKey, new SecureRandom()); + } else { + response = RSASHA1Verify.get().generateSignature(challenge, rsaPrivKey, new SecureRandom()); + } + } else if (privKey instanceof DSAPrivateKey) { + response = DSASHA1Verify.get().generateSignature(challenge, privKey, new SecureRandom()); + } else if (privKey instanceof Ed25519PrivateKey) { + response = Ed25519Verify.get().generateSignature(challenge, privKey, new SecureRandom()); + } else { + os.write(SSH_AGENT_FAILURE); + return; + } + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_SIGN_RESPONSE); + tw.writeString(response, 0, response.length); + + sendPacket(tw.getBytes()); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void processLockRequest(TypesReader tr) { + try + { + if (failWhenLocked()) + return; + + String lockPassphrase = tr.readString(); + if (!authAgent.setAgentLock(lockPassphrase)) { + os.write(SSH_AGENT_FAILURE); + return; + } else + os.write(SSH_AGENT_SUCCESS); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void processUnlockRequest(TypesReader tr) + { + try + { + String unlockPassphrase = tr.readString(); + + if (authAgent.requestAgentUnlock(unlockPassphrase)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param message + * @throws IOException + */ + private void sendPacket(byte[] message) throws IOException + { + TypesWriter packet = new TypesWriter(); + packet.writeUINT32(message.length); + packet.writeBytes(message); + os.write(packet.getBytes()); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/Channel.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/Channel.java new file mode 100644 index 0000000000..c6d4354cf3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/Channel.java @@ -0,0 +1,207 @@ + +package com.trilead.ssh2.channel; + +/** + * Channel. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Channel.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class Channel +{ + /* + * OK. Here is an important part of the JVM Specification: + * (http://java.sun.com/docs/books/vmspec/2nd-edition/html/Threads.doc.html#22214) + * + * Any association between locks and variables is purely conventional. + * Locking any lock conceptually flushes all variables from a thread's + * working memory, and unlocking any lock forces the writing out to main + * memory of all variables that the thread has assigned. That a lock may be + * associated with a particular object or a class is purely a convention. + * (...) + * + * If a thread uses a particular shared variable only after locking a + * particular lock and before the corresponding unlocking of that same lock, + * then the thread will read the shared value of that variable from main + * memory after the lock operation, if necessary, and will copy back to main + * memory the value most recently assigned to that variable before the + * unlock operation. + * + * This, in conjunction with the mutual exclusion rules for locks, suffices + * to guarantee that values are correctly transmitted from one thread to + * another through shared variables. + * + * ====> Always keep that in mind when modifying the Channel/ChannelManger + * code. + * + */ + + static final int STATE_OPENING = 1; + static final int STATE_OPEN = 2; + static final int STATE_CLOSED = 4; + + static final int CHANNEL_BUFFER_SIZE = 30000; + + /* + * To achieve correctness, the following rules have to be respected when + * accessing this object: + */ + + // These fields can always be read + final ChannelManager cm; + final ChannelOutputStream stdinStream; + final ChannelInputStream stdoutStream; + final ChannelInputStream stderrStream; + + // These two fields will only be written while the Channel is in state + // STATE_OPENING. + // The code makes sure that the two fields are written out when the state is + // changing to STATE_OPEN. + // Therefore, if you know that the Channel is in state STATE_OPEN, then you + // can read these two fields without synchronizing on the Channel. However, make + // sure that you get the latest values (e.g., flush caches by synchronizing on any + // object). However, to be on the safe side, you can lock the channel. + + int localID = -1; + int remoteID = -1; + + /* + * Make sure that we never send a data/EOF/WindowChange msg after a CLOSE + * msg. + * + * This is a little bit complicated, but we have to do it in that way, since + * we cannot keep a lock on the Channel during the send operation (this + * would block sometimes the receiver thread, and, in extreme cases, can + * lead to a deadlock on both sides of the connection (senders are blocked + * since the receive buffers on the other side are full, and receiver + * threads wait for the senders to finish). It all depends on the + * implementation on the other side. But we cannot make any assumptions, we + * have to assume the worst case. Confused? Just believe me. + */ + + /* + * If you send a message on a channel, then you have to aquire the + * "channelSendLock" and check the "closeMessageSent" flag (this variable + * may only be accessed while holding the "channelSendLock" !!! + * + * BTW: NEVER EVER SEND MESSAGES FROM THE RECEIVE THREAD - see explanation + * above. + */ + + final Object channelSendLock = new Object(); + boolean closeMessageSent = false; + + /* + * Stop memory fragmentation by allocating this often used buffer. + * May only be used while holding the channelSendLock + */ + + final byte[] msgWindowAdjust = new byte[9]; + + // If you access (read or write) any of the following fields, then you have + // to synchronize on the channel. + + int state = STATE_OPENING; + + boolean closeMessageRecv = false; + + /* This is a stupid implementation. At the moment we can only wait + * for one pending request per channel. + */ + int successCounter = 0; + int failedCounter = 0; + + int localWindow = 0; /* locally, we use a small window, < 2^31 */ + long remoteWindow = 0; /* long for readable 2^32 - 1 window support */ + + int localMaxPacketSize = -1; + int remoteMaxPacketSize = -1; + + final byte[] stdoutBuffer = new byte[CHANNEL_BUFFER_SIZE]; + final byte[] stderrBuffer = new byte[CHANNEL_BUFFER_SIZE]; + + int stdoutReadpos = 0; + int stdoutWritepos = 0; + int stderrReadpos = 0; + int stderrWritepos = 0; + + boolean EOF = false; + + Integer exit_status; + + String exit_signal; + + // we keep the x11 cookie so that this channel can be closed when this + // specific x11 forwarding gets stopped + + String hexX11FakeCookie; + + // reasonClosed is special, since we sometimes need to access it + // while holding the channelSendLock. + // We protect it with a private short term lock. + + private final Object reasonClosedLock = new Object(); + private String reasonClosed = null; + + public Channel(ChannelManager cm) + { + this.cm = cm; + + this.localWindow = CHANNEL_BUFFER_SIZE; + this.localMaxPacketSize = 35000 - 1024; // leave enough slack + + this.stdinStream = new ChannelOutputStream(this); + this.stdoutStream = new ChannelInputStream(this, false); + this.stderrStream = new ChannelInputStream(this, true); + } + + /* Methods to allow access from classes outside of this package */ + + public ChannelInputStream getStderrStream() + { + return stderrStream; + } + + public ChannelOutputStream getStdinStream() + { + return stdinStream; + } + + public ChannelInputStream getStdoutStream() + { + return stdoutStream; + } + + public String getExitSignal() + { + synchronized (this) + { + return exit_signal; + } + } + + public Integer getExitStatus() + { + synchronized (this) + { + return exit_status; + } + } + + public String getReasonClosed() + { + synchronized (reasonClosedLock) + { + return reasonClosed; + } + } + + public void setReasonClosed(String reasonClosed) + { + synchronized (reasonClosedLock) + { + if (this.reasonClosed == null) + this.reasonClosed = reasonClosed; + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java new file mode 100644 index 0000000000..e1f6a9c1e2 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java @@ -0,0 +1,85 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; + +/** + * ChannelInputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelInputStream.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public final class ChannelInputStream extends InputStream +{ + Channel c; + + boolean isClosed = false; + boolean isEOF = false; + boolean extendedFlag = false; + + ChannelInputStream(Channel c, boolean isExtended) + { + this.c = c; + this.extendedFlag = isExtended; + } + + public int available() throws IOException + { + if (isEOF) + return 0; + + int avail = c.cm.getAvailable(c, extendedFlag); + + /* We must not return -1 on EOF */ + + return (avail > 0) ? avail : 0; + } + + public void close() { + isClosed = true; + } + + public int read(byte[] b, int off, int len) throws IOException + { + if (b == null) + throw new NullPointerException(); + + if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) + throw new IndexOutOfBoundsException(); + + if (len == 0) + return 0; + + if (isEOF) + return -1; + + int ret = c.cm.getChannelData(c, extendedFlag, b, off, len); + + if (ret == -1) + { + isEOF = true; + } + + return ret; + } + + public int read(byte[] b) throws IOException + { + return read(b, 0, b.length); + } + + public int read() throws IOException + { + /* Yes, this stream is pure and unbuffered, a single byte read() is slow */ + + final byte b[] = new byte[1]; + + int ret = read(b, 0, 1); + + if (ret != 1) + return -1; + + return b[0] & 0xff; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelManager.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelManager.java new file mode 100644 index 0000000000..7858a57af6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelManager.java @@ -0,0 +1,1749 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.ChannelCondition; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketChannelAuthAgentReq; +import com.trilead.ssh2.packets.PacketChannelOpenConfirmation; +import com.trilead.ssh2.packets.PacketChannelOpenFailure; +import com.trilead.ssh2.packets.PacketChannelTrileadPing; +import com.trilead.ssh2.packets.PacketGlobalCancelForwardRequest; +import com.trilead.ssh2.packets.PacketGlobalForwardRequest; +import com.trilead.ssh2.packets.PacketGlobalTrileadPing; +import com.trilead.ssh2.packets.PacketOpenDirectTCPIPChannel; +import com.trilead.ssh2.packets.PacketOpenSessionChannel; +import com.trilead.ssh2.packets.PacketSessionExecCommand; +import com.trilead.ssh2.packets.PacketSessionPtyRequest; +import com.trilead.ssh2.packets.PacketSessionPtyResize; +import com.trilead.ssh2.packets.PacketSessionStartShell; +import com.trilead.ssh2.packets.PacketSessionSubsystemRequest; +import com.trilead.ssh2.packets.PacketSessionX11Request; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.transport.MessageHandler; +import com.trilead.ssh2.transport.TransportManager; + +/** + * ChannelManager. Please read the comments in Channel.java. + *

+ * Besides the crypto part, this is the core of the library. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelManager.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $ + */ +public class ChannelManager implements MessageHandler +{ + private static final Logger log = Logger.getLogger(ChannelManager.class); + + private final HashMap x11_magic_cookies = new HashMap<>(); + + private TransportManager tm; + + private final List channels = new ArrayList<>(); + private int nextLocalChannel = 100; + private boolean shutdown = false; + private int globalSuccessCounter = 0; + private int globalFailedCounter = 0; + + private final HashMap remoteForwardings = new HashMap<>(); + + private AuthAgentCallback authAgent; + + private final List listenerThreads = new ArrayList<>(); + + private boolean listenerThreadsAllowed = true; + + public ChannelManager(TransportManager tm) + { + this.tm = tm; + tm.registerMessageHandler(this, 80, 100); + } + + private Channel getChannel(int id) + { + synchronized (channels) + { + for (Channel c : channels) + { + if (c.localID == id) + return c; + } + } + return null; + } + + private void removeChannel(int id) + { + synchronized (channels) + { + for (int i = 0; i < channels.size(); i++) + { + Channel c = channels.get(i); + if (c.localID == id) + { + channels.remove(i); + break; + } + } + } + } + + private int addChannel(Channel c) + { + synchronized (channels) + { + channels.add(c); + return nextLocalChannel++; + } + } + + private void waitUntilChannelOpen(Channel c) throws IOException + { + synchronized (c) + { + while (c.state == Channel.STATE_OPENING) + { + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + if (c.state != Channel.STATE_OPEN) + { + removeChannel(c.localID); + + String detail = c.getReasonClosed(); + + if (detail == null) + detail = "state: " + c.state; + + throw new IOException("Could not open channel (" + detail + ")"); + } + } + } + + private boolean waitForGlobalRequestResult() throws IOException + { + synchronized (channels) + { + while ((globalSuccessCounter == 0) && (globalFailedCounter == 0)) + { + if (shutdown) + { + throw new IOException("The connection is being shutdown"); + } + + try + { + channels.wait(); + } + catch (InterruptedException ignore) + { + } + } + + if ((globalFailedCounter == 0) && (globalSuccessCounter == 1)) + return true; + + if ((globalFailedCounter == 1) && (globalSuccessCounter == 0)) + return false; + + throw new IOException("Illegal state. The server sent " + globalSuccessCounter + + " SSH_MSG_REQUEST_SUCCESS and " + globalFailedCounter + " SSH_MSG_REQUEST_FAILURE messages."); + } + } + + private boolean waitForChannelRequestResult(Channel c) throws IOException + { + synchronized (c) + { + while ((c.successCounter == 0) && (c.failedCounter == 0)) + { + if (c.state != Channel.STATE_OPEN) + { + String detail = c.getReasonClosed(); + + if (detail == null) + detail = "state: " + c.state; + + throw new IOException("This SSH2 channel is not open (" + detail + ")"); + } + + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + if ((c.failedCounter == 0) && (c.successCounter == 1)) + return true; + + if ((c.failedCounter == 1) && (c.successCounter == 0)) + return false; + + throw new IOException("Illegal state. The server sent " + c.successCounter + + " SSH_MSG_CHANNEL_SUCCESS and " + c.failedCounter + " SSH_MSG_CHANNEL_FAILURE messages."); + } + } + + public void registerX11Cookie(String hexFakeCookie, X11ServerData data) + { + synchronized (x11_magic_cookies) + { + x11_magic_cookies.put(hexFakeCookie, data); + } + } + + public void unRegisterX11Cookie(String hexFakeCookie, boolean killChannels) + { + if (hexFakeCookie == null) + throw new IllegalStateException("hexFakeCookie may not be null"); + + synchronized (x11_magic_cookies) + { + x11_magic_cookies.remove(hexFakeCookie); + } + + if (!killChannels) + return; + + if (log.isEnabled()) + log.log(50, "Closing all X11 channels for the given fake cookie"); + + List channel_copy; + + synchronized (channels) + { + channel_copy = new ArrayList<>(channels); + } + + for (int i = 0; i < channel_copy.size(); i++) + { + Channel c = channel_copy.get(i); + + synchronized (c) + { + if (!hexFakeCookie.equals(c.hexX11FakeCookie)) + continue; + } + + try + { + closeChannel(c, "Closing X11 channel since the corresponding session is closing", true); + } + catch (IOException e) + { + } + } + } + + public X11ServerData checkX11Cookie(String hexFakeCookie) + { + synchronized (x11_magic_cookies) + { + if (hexFakeCookie != null) + return x11_magic_cookies.get(hexFakeCookie); + } + return null; + } + + public void closeAllChannels() + { + if (log.isEnabled()) + log.log(50, "Closing all channels"); + + List channel_copy; + + synchronized (channels) + { + channel_copy = new ArrayList<>(channels); + } + + for (int i = 0; i < channel_copy.size(); i++) + { + Channel c = channel_copy.get(i); + try + { + closeChannel(c, "Closing all channels", true); + } + catch (IOException e) + { + } + } + } + + public void closeChannel(Channel c, String reason, boolean force) throws IOException + { + byte msg[] = new byte[5]; + + synchronized (c) + { + if (force) + { + c.state = Channel.STATE_CLOSED; + c.EOF = true; + } + + c.setReasonClosed(reason); + + msg[0] = Packets.SSH_MSG_CHANNEL_CLOSE; + msg[1] = (byte) (c.remoteID >> 24); + msg[2] = (byte) (c.remoteID >> 16); + msg[3] = (byte) (c.remoteID >> 8); + msg[4] = (byte) (c.remoteID); + + c.notifyAll(); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + return; + tm.sendMessage(msg); + c.closeMessageSent = true; + } + + if (log.isEnabled()) + log.log(50, "Sent SSH_MSG_CHANNEL_CLOSE (channel " + c.localID + ")"); + } + + public void sendEOF(Channel c) throws IOException + { + byte[] msg = new byte[5]; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + return; + + msg[0] = Packets.SSH_MSG_CHANNEL_EOF; + msg[1] = (byte) (c.remoteID >> 24); + msg[2] = (byte) (c.remoteID >> 16); + msg[3] = (byte) (c.remoteID >> 8); + msg[4] = (byte) (c.remoteID); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + return; + tm.sendMessage(msg); + } + + if (log.isEnabled()) + log.log(50, "Sent EOF (Channel " + c.localID + "/" + c.remoteID + ")"); + } + + public void sendOpenConfirmation(Channel c) throws IOException + { + PacketChannelOpenConfirmation pcoc = null; + + synchronized (c) + { + if (c.state != Channel.STATE_OPENING) + return; + + c.state = Channel.STATE_OPEN; + + pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID, c.localWindow, c.localMaxPacketSize); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + return; + tm.sendMessage(pcoc.getPayload()); + } + } + + public void sendData(Channel c, byte[] buffer, int pos, int len) throws IOException + { + while (len > 0) + { + int thislen = 0; + byte[] msg; + + synchronized (c) + { + while (true) + { + if (c.state == Channel.STATE_CLOSED) + throw new IOException("SSH channel is closed. (" + c.getReasonClosed() + ")"); + + if (c.state != Channel.STATE_OPEN) + throw new IOException("SSH channel in strange state. (" + c.state + ")"); + + if (c.remoteWindow != 0) + break; + + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + /* len > 0, no sign extension can happen when comparing */ + + thislen = (c.remoteWindow >= len) ? len : (int) c.remoteWindow; + + int estimatedMaxDataLen = c.remoteMaxPacketSize - (tm.getPacketOverheadEstimate() + 9); + + /* The worst case scenario =) a true bottleneck */ + + if (estimatedMaxDataLen <= 0) + { + estimatedMaxDataLen = 1; + } + + if (thislen > estimatedMaxDataLen) + thislen = estimatedMaxDataLen; + + c.remoteWindow -= thislen; + + msg = new byte[1 + 8 + thislen]; + + msg[0] = Packets.SSH_MSG_CHANNEL_DATA; + msg[1] = (byte) (c.remoteID >> 24); + msg[2] = (byte) (c.remoteID >> 16); + msg[3] = (byte) (c.remoteID >> 8); + msg[4] = (byte) (c.remoteID); + msg[5] = (byte) (thislen >> 24); + msg[6] = (byte) (thislen >> 16); + msg[7] = (byte) (thislen >> 8); + msg[8] = (byte) (thislen); + + System.arraycopy(buffer, pos, msg, 9, thislen); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("SSH channel is closed. (" + c.getReasonClosed() + ")"); + + tm.sendMessage(msg); + } + + pos += thislen; + len -= thislen; + } + } + + public int requestGlobalForward(String bindAddress, int bindPort, String targetAddress, int targetPort) + throws IOException + { + RemoteForwardingData rfd = new RemoteForwardingData(); + + rfd.bindAddress = bindAddress; + rfd.bindPort = bindPort; + rfd.targetAddress = targetAddress; + rfd.targetPort = targetPort; + + synchronized (remoteForwardings) + { + if (remoteForwardings.get(bindPort) != null) + { + throw new IOException("There is already a forwarding for remote port " + bindPort); + } + + remoteForwardings.put(bindPort, rfd); + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + PacketGlobalForwardRequest pgf = new PacketGlobalForwardRequest(true, bindAddress, bindPort); + tm.sendMessage(pgf.getPayload()); + + if (log.isEnabled()) + log.log(50, "Requesting a remote forwarding ('" + bindAddress + "', " + bindPort + ")"); + + try + { + if (!waitForGlobalRequestResult()) + throw new IOException("The server denied the request (did you enable port forwarding?)"); + } + catch (IOException e) + { + synchronized (remoteForwardings) + { + remoteForwardings.remove(rfd.bindPort); + } + throw e; + } + + return bindPort; + } + + public void requestCancelGlobalForward(int bindPort) throws IOException + { + RemoteForwardingData rfd = null; + + synchronized (remoteForwardings) + { + rfd = remoteForwardings.get(bindPort); + + if (rfd == null) + throw new IOException("Sorry, there is no known remote forwarding for remote port " + bindPort); + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + PacketGlobalCancelForwardRequest pgcf = new PacketGlobalCancelForwardRequest(true, rfd.bindAddress, + rfd.bindPort); + tm.sendMessage(pgcf.getPayload()); + + if (log.isEnabled()) + log.log(50, "Requesting cancelation of remote forward ('" + rfd.bindAddress + "', " + rfd.bindPort + ")"); + + try + { + if (!waitForGlobalRequestResult()) + throw new IOException("The server denied the request."); + } + finally + { + synchronized (remoteForwardings) + { + /* Only now we are sure that no more forwarded connections will arrive */ + remoteForwardings.remove(rfd.bindPort); + } + } + + } + + /** + * @param c + * @param authAgent + * @throws IOException + */ + public boolean requestChannelAgentForwarding(Channel c, AuthAgentCallback authAgent) throws IOException { + synchronized (this) + { + if (this.authAgent != null) + throw new IllegalStateException("Auth agent already exists"); + + this.authAgent = authAgent; + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + if (log.isEnabled()) + log.log(50, "Requesting agent forwarding"); + + PacketChannelAuthAgentReq aar = new PacketChannelAuthAgentReq(c.remoteID); + tm.sendMessage(aar.getPayload()); + + if (!waitForChannelRequestResult(c)) { + authAgent = null; + return false; + } + + return true; + } + + public void registerThread(IChannelWorkerThread thr) throws IOException + { + synchronized (listenerThreads) + { + if (!listenerThreadsAllowed) + throw new IOException("Too late, this connection is closed."); + listenerThreads.add(thr); + } + } + + public Channel openDirectTCPIPChannel(String host_to_connect, int port_to_connect, String originator_IP_address, + int originator_port) throws IOException + { + Channel c = new Channel(this); + + synchronized (c) + { + c.localID = addChannel(c); + // end of synchronized block forces writing out to main memory + } + + PacketOpenDirectTCPIPChannel dtc = new PacketOpenDirectTCPIPChannel(c.localID, c.localWindow, + c.localMaxPacketSize, host_to_connect, port_to_connect, originator_IP_address, originator_port); + + tm.sendMessage(dtc.getPayload()); + + waitUntilChannelOpen(c); + + return c; + } + + public Channel openSessionChannel() throws IOException + { + Channel c = new Channel(this); + + synchronized (c) + { + c.localID = addChannel(c); + // end of synchronized block forces the writing out to main memory + } + + if (log.isEnabled()) + log.log(50, "Sending SSH_MSG_CHANNEL_OPEN (Channel " + c.localID + ")"); + + PacketOpenSessionChannel smo = new PacketOpenSessionChannel(c.localID, c.localWindow, c.localMaxPacketSize); + tm.sendMessage(smo.getPayload()); + + waitUntilChannelOpen(c); + + return c; + } + + public void requestGlobalTrileadPing() throws IOException + { + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + PacketGlobalTrileadPing pgtp = new PacketGlobalTrileadPing(); + + tm.sendMessage(pgtp.getPayload()); + + if (log.isEnabled()) + log.log(50, "Sending SSH_MSG_GLOBAL_REQUEST 'trilead-ping'."); + + try + { + if (waitForGlobalRequestResult()) + throw new IOException("Your server is alive - but buggy. " + + "It replied with SSH_MSG_REQUEST_SUCCESS when it actually should not."); + + } + catch (IOException e) + { + throw new IOException("The ping request failed.", e); + } + } + + public void requestChannelTrileadPing(Channel c) throws IOException + { + PacketChannelTrileadPing pctp; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot ping this channel (" + c.getReasonClosed() + ")"); + + pctp = new PacketChannelTrileadPing(c.remoteID); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot ping this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(pctp.getPayload()); + } + + try + { + if (waitForChannelRequestResult(c)) + throw new IOException("Your server is alive - but buggy. " + + "It replied with SSH_MSG_SESSION_SUCCESS when it actually should not."); + + } + catch (IOException e) + { + throw new IOException("The ping request failed.", e); + } + } + + public void requestPTY(Channel c, String term, int term_width_characters, int term_height_characters, + int term_width_pixels, int term_height_pixels, byte[] terminal_modes) throws IOException + { + PacketSessionPtyRequest spr; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")"); + + spr = new PacketSessionPtyRequest(c.remoteID, true, term, term_width_characters, term_height_characters, + term_width_pixels, term_height_pixels, terminal_modes); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(spr.getPayload()); + } + + try + { + if (!waitForChannelRequestResult(c)) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw new IOException("PTY request failed", e); + } + } + + + public void resizePTY(Channel c, int term_width_characters, int term_height_characters, + int term_width_pixels, int term_height_pixels) throws IOException { + PacketSessionPtyResize spr; + + synchronized (c) { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request PTY on this channel (" + + c.getReasonClosed() + ")"); + + spr = new PacketSessionPtyResize(c.remoteID, term_width_characters, term_height_characters, + term_width_pixels, term_height_pixels); + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) { + if (c.closeMessageSent) + throw new IOException("Cannot request PTY on this channel (" + + c.getReasonClosed() + ")"); + tm.sendMessage(spr.getPayload()); + } + } + + + public void requestX11(Channel c, boolean singleConnection, String x11AuthenticationProtocol, + String x11AuthenticationCookie, int x11ScreenNumber) throws IOException + { + PacketSessionX11Request psr; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")"); + + psr = new PacketSessionX11Request(c.remoteID, true, singleConnection, x11AuthenticationProtocol, + x11AuthenticationCookie, x11ScreenNumber); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(psr.getPayload()); + } + + if (log.isEnabled()) + log.log(50, "Requesting X11 forwarding (Channel " + c.localID + "/" + c.remoteID + ")"); + + try + { + if (!waitForChannelRequestResult(c)) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw new IOException("The X11 request failed.", e); + } + } + + public void requestSubSystem(Channel c, String subSystemName) throws IOException + { + PacketSessionSubsystemRequest ssr; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")"); + + ssr = new PacketSessionSubsystemRequest(c.remoteID, true, subSystemName); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(ssr.getPayload()); + } + + try + { + if (!waitForChannelRequestResult(c)) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw new IOException("The subsystem request failed.", e); + } + } + + public void requestExecCommand(Channel c, String cmd) throws IOException + { + PacketSessionExecCommand sm; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")"); + + sm = new PacketSessionExecCommand(c.remoteID, true, cmd); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(sm.getPayload()); + } + + if (log.isEnabled()) + log.log(50, "Executing command (channel " + c.localID + ", '" + cmd + "')"); + + try + { + if (!waitForChannelRequestResult(c)) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw new IOException("The execute request failed.", e); + } + } + + public void requestShell(Channel c) throws IOException + { + PacketSessionStartShell sm; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")"); + + sm = new PacketSessionStartShell(c.remoteID, true); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(sm.getPayload()); + } + + try + { + if (!waitForChannelRequestResult(c)) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw new IOException("The shell request failed.", e); + } + } + + public void msgChannelExtendedData(byte[] msg, int msglen) throws IOException + { + if (msglen <= 13) + throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + int dataType = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); + int len = ((msg[9] & 0xff) << 24) | ((msg[10] & 0xff) << 16) | ((msg[11] & 0xff) << 8) | (msg[12] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_EXTENDED_DATA message for non-existent channel " + id); + + if (dataType != Packets.SSH_EXTENDED_DATA_STDERR) + throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has unknown type (" + dataType + ")"); + + if (len != (msglen - 13)) + throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong len (calculated " + (msglen - 13) + + ", got " + len + ")"); + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_EXTENDED_DATA (channel " + id + ", " + len + ")"); + + synchronized (c) + { + if (c.state == Channel.STATE_CLOSED) + return; // ignore + + if (c.state != Channel.STATE_OPEN) + throw new IOException("Got SSH_MSG_CHANNEL_EXTENDED_DATA, but channel is not in correct state (" + + c.state + ")"); + + if (c.localWindow < len) + throw new IOException("Remote sent too much data, does not fit into window."); + + c.localWindow -= len; + + System.arraycopy(msg, 13, c.stderrBuffer, c.stderrWritepos, len); + c.stderrWritepos += len; + + c.notifyAll(); + } + } + + /** + * Wait until for a condition. + * + * @param c + * Channel + * @param timeout + * in ms, 0 means no timeout. + * @param condition_mask + * minimum event mask + * @return all current events + * + */ + public int waitForCondition(Channel c, long timeout, int condition_mask) + { + long end_time = 0; + boolean end_time_set = false; + + synchronized (c) + { + while (true) + { + int current_cond = 0; + + int stdoutAvail = c.stdoutWritepos - c.stdoutReadpos; + int stderrAvail = c.stderrWritepos - c.stderrReadpos; + + if (stdoutAvail > 0) + current_cond = current_cond | ChannelCondition.STDOUT_DATA; + + if (stderrAvail > 0) + current_cond = current_cond | ChannelCondition.STDERR_DATA; + + if (c.EOF) + current_cond = current_cond | ChannelCondition.EOF; + + if (c.getExitStatus() != null) + current_cond = current_cond | ChannelCondition.EXIT_STATUS; + + if (c.getExitSignal() != null) + current_cond = current_cond | ChannelCondition.EXIT_SIGNAL; + + if (c.state == Channel.STATE_CLOSED) + return current_cond | ChannelCondition.CLOSED | ChannelCondition.EOF; + + if ((current_cond & condition_mask) != 0) + return current_cond; + + if (timeout > 0) + { + if (!end_time_set) + { + end_time = System.currentTimeMillis() + timeout; + end_time_set = true; + } + else + { + timeout = end_time - System.currentTimeMillis(); + + if (timeout <= 0) + return current_cond | ChannelCondition.TIMEOUT; + } + } + + try + { + if (timeout > 0) + c.wait(timeout); + else + c.wait(); + } + catch (InterruptedException e) + { + } + } + } + } + + public int getAvailable(Channel c, boolean extended) { + synchronized (c) + { + int avail; + + if (extended) + avail = c.stderrWritepos - c.stderrReadpos; + else + avail = c.stdoutWritepos - c.stdoutReadpos; + + return ((avail > 0) ? avail : (c.EOF ? -1 : 0)); + } + } + + public int getChannelData(Channel c, boolean extended, byte[] target, int off, int len) throws IOException + { + int copylen = 0; + int increment = 0; + int remoteID = 0; + int localID = 0; + + synchronized (c) + { + int stdoutAvail = 0; + int stderrAvail = 0; + + while (true) + { + /* + * Data available? We have to return remaining data even if the + * channel is already closed. + */ + + stdoutAvail = c.stdoutWritepos - c.stdoutReadpos; + stderrAvail = c.stderrWritepos - c.stderrReadpos; + + if ((!extended) && (stdoutAvail != 0)) + break; + + if ((extended) && (stderrAvail != 0)) + break; + + /* Do not wait if more data will never arrive (EOF or CLOSED) */ + + if ((c.EOF) || (c.state != Channel.STATE_OPEN)) + return -1; + + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + /* OK, there is some data. Return it. */ + + if (!extended) + { + copylen = (stdoutAvail > len) ? len : stdoutAvail; + System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, target, off, copylen); + c.stdoutReadpos += copylen; + + if (c.stdoutReadpos != c.stdoutWritepos) + + System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, c.stdoutBuffer, 0, c.stdoutWritepos + - c.stdoutReadpos); + + c.stdoutWritepos -= c.stdoutReadpos; + c.stdoutReadpos = 0; + } + else + { + copylen = (stderrAvail > len) ? len : stderrAvail; + System.arraycopy(c.stderrBuffer, c.stderrReadpos, target, off, copylen); + c.stderrReadpos += copylen; + + if (c.stderrReadpos != c.stderrWritepos) + + System.arraycopy(c.stderrBuffer, c.stderrReadpos, c.stderrBuffer, 0, c.stderrWritepos + - c.stderrReadpos); + + c.stderrWritepos -= c.stderrReadpos; + c.stderrReadpos = 0; + } + + if (c.state != Channel.STATE_OPEN) + return copylen; + + if (c.localWindow < ((Channel.CHANNEL_BUFFER_SIZE + 1) / 2)) + { + int minFreeSpace = Math.min(Channel.CHANNEL_BUFFER_SIZE - c.stdoutWritepos, Channel.CHANNEL_BUFFER_SIZE + - c.stderrWritepos); + + increment = minFreeSpace - c.localWindow; + c.localWindow = minFreeSpace; + } + + remoteID = c.remoteID; /* read while holding the lock */ + localID = c.localID; /* read while holding the lock */ + } + + /* + * If a consumer reads stdout and stdin in parallel, we may end up with + * sending two msgWindowAdjust messages. Luckily, it + * does not matter in which order they arrive at the server. + */ + + if (increment > 0) + { + if (log.isEnabled()) + log.log(80, "Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + localID + ", " + increment + ")"); + + synchronized (c.channelSendLock) + { + byte[] msg = c.msgWindowAdjust; + + msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST; + msg[1] = (byte) (remoteID >> 24); + msg[2] = (byte) (remoteID >> 16); + msg[3] = (byte) (remoteID >> 8); + msg[4] = (byte) (remoteID); + msg[5] = (byte) (increment >> 24); + msg[6] = (byte) (increment >> 16); + msg[7] = (byte) (increment >> 8); + msg[8] = (byte) (increment); + + if (!c.closeMessageSent) + tm.sendMessage(msg); + } + } + + return copylen; + } + + public void msgChannelData(byte[] msg, int msglen) throws IOException + { + if (msglen <= 9) + throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + int len = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_DATA message for non-existent channel " + id); + + if (len != (msglen - 9)) + throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong len (calculated " + (msglen - 9) + ", got " + + len + ")"); + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_DATA (channel " + id + ", " + len + ")"); + + synchronized (c) + { + if (c.state == Channel.STATE_CLOSED) + return; // ignore + + if (c.state != Channel.STATE_OPEN) + throw new IOException("Got SSH_MSG_CHANNEL_DATA, but channel is not in correct state (" + c.state + ")"); + + if (c.localWindow < len) + throw new IOException("Remote sent too much data, does not fit into window."); + + c.localWindow -= len; + + System.arraycopy(msg, 9, c.stdoutBuffer, c.stdoutWritepos, len); + c.stdoutWritepos += len; + + c.notifyAll(); + } + } + + public void msgChannelWindowAdjust(byte[] msg, int msglen) throws IOException + { + if (msglen != 9) + throw new IOException("SSH_MSG_CHANNEL_WINDOW_ADJUST message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + int windowChange = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_WINDOW_ADJUST message for non-existent channel " + id); + + synchronized (c) + { + final long huge = 0xFFFFffffL; /* 2^32 - 1 */ + + c.remoteWindow += (windowChange & huge); /* avoid sign extension */ + + /* TODO - is this a good heuristic? */ + + if ((c.remoteWindow > huge)) + c.remoteWindow = huge; + + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + id + ", " + windowChange + ")"); + } + + public void msgChannelOpen(byte[] msg, int msglen) throws IOException + { + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + String channelType = tr.readString(); + int remoteID = tr.readUINT32(); /* sender channel */ + int remoteWindow = tr.readUINT32(); /* initial window size */ + int remoteMaxPacketSize = tr.readUINT32(); /* maximum packet size */ + + if ("x11".equals(channelType)) + { + synchronized (x11_magic_cookies) + { + /* If we did not request X11 forwarding, then simply ignore this bogus request. */ + + if (x11_magic_cookies.size() == 0) + { + PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, + Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "X11 forwarding not activated", ""); + + tm.sendAsynchronousMessage(pcof.getPayload()); + + if (log.isEnabled()) + log.log(20, "Unexpected X11 request, denying it!"); + + return; + } + } + + String remoteOriginatorAddress = tr.readString(); + int remoteOriginatorPort = tr.readUINT32(); + + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + /* + * The open confirmation message will be sent from another thread + */ + + RemoteX11AcceptThread rxat = new RemoteX11AcceptThread(c, remoteOriginatorAddress, remoteOriginatorPort); + rxat.setDaemon(true); + rxat.start(); + + return; + } + + if ("forwarded-tcpip".equals(channelType)) + { + String remoteConnectedAddress = tr.readString(); /* address that was connected */ + int remoteConnectedPort = tr.readUINT32(); /* port that was connected */ + String remoteOriginatorAddress = tr.readString(); /* originator IP address */ + int remoteOriginatorPort = tr.readUINT32(); /* originator port */ + + RemoteForwardingData rfd = null; + + synchronized (remoteForwardings) + { + rfd = remoteForwardings.get(Integer.valueOf(remoteConnectedPort)); + } + + if (rfd == null) + { + PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, + Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, + "No thanks, unknown port in forwarded-tcpip request", ""); + + /* Always try to be polite. */ + + tm.sendAsynchronousMessage(pcof.getPayload()); + + if (log.isEnabled()) + log.log(20, "Unexpected forwarded-tcpip request, denying it!"); + + return; + } + + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + /* + * The open confirmation message will be sent from another thread. + */ + + RemoteAcceptThread rat = new RemoteAcceptThread(c, remoteConnectedAddress, remoteConnectedPort, + remoteOriginatorAddress, remoteOriginatorPort, rfd.targetAddress, rfd.targetPort); + + rat.setDaemon(true); + rat.start(); + + return; + } + + if ("auth-agent@openssh.com".equals(channelType)) { + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + AuthAgentForwardThread aat = new AuthAgentForwardThread(c, authAgent); + + aat.setDaemon(true); + aat.start(); + + return; + } + + /* Tell the server that we have no idea what it is talking about */ + + PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, + "Unknown channel type", ""); + + tm.sendAsynchronousMessage(pcof.getPayload()); + + if (log.isEnabled()) + log.log(20, "The peer tried to open an unsupported channel type (" + channelType + ")"); + } + + public void msgChannelRequest(byte[] msg, int msglen) throws IOException + { + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + int id = tr.readUINT32(); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_REQUEST message for non-existent channel " + id); + + String type = tr.readString("US-ASCII"); + boolean wantReply = tr.readBoolean(); + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_REQUEST (channel " + id + ", '" + type + "')"); + + if (type.equals("exit-status")) + { + if (wantReply) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message, 'want reply' is true"); + + int exit_status = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); + + synchronized (c) + { + c.exit_status = Integer.valueOf(exit_status); + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got EXIT STATUS (channel " + id + ", status " + exit_status + ")"); + + return; + } + + if (type.equals("exit-signal")) + { + if (wantReply) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message, 'want reply' is true"); + + String signame = tr.readString("US-ASCII"); + tr.readBoolean(); + tr.readString(); + tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); + + synchronized (c) + { + c.exit_signal = signame; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got EXIT SIGNAL (channel " + id + ", signal " + signame + ")"); + + return; + } + + /* We simply ignore unknown channel requests, however, if the server wants a reply, + * then we signal that we have no idea what it is about. + */ + + if (wantReply) + { + byte[] reply = new byte[5]; + + reply[0] = Packets.SSH_MSG_CHANNEL_FAILURE; + reply[1] = (byte) (c.remoteID >> 24); + reply[2] = (byte) (c.remoteID >> 16); + reply[3] = (byte) (c.remoteID >> 8); + reply[4] = (byte) (c.remoteID); + + tm.sendAsynchronousMessage(reply); + } + + if (log.isEnabled()) + log.log(50, "Channel request '" + type + "' is not known, ignoring it"); + } + + public void msgChannelEOF(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_EOF message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_EOF message for non-existent channel " + id); + + synchronized (c) + { + c.EOF = true; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_EOF (channel " + id + ")"); + } + + public void msgChannelClose(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_CLOSE message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_CLOSE message for non-existent channel " + id); + + synchronized (c) + { + c.EOF = true; + c.state = Channel.STATE_CLOSED; + c.setReasonClosed("Close requested by remote"); + c.closeMessageRecv = true; + + removeChannel(c.localID); + + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_CLOSE (channel " + id + ")"); + } + + public void msgChannelSuccess(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_SUCCESS message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_SUCCESS message for non-existent channel " + id); + + synchronized (c) + { + c.successCounter++; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_SUCCESS (channel " + id + ")"); + } + + public void msgChannelFailure(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_FAILURE message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_FAILURE message for non-existent channel " + id); + + synchronized (c) + { + c.failedCounter++; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_FAILURE (channel " + id + ")"); + } + + public void msgChannelOpenConfirmation(byte[] msg, int msglen) throws IOException + { + PacketChannelOpenConfirmation sm = new PacketChannelOpenConfirmation(msg, 0, msglen); + + Channel c = getChannel(sm.recipientChannelID); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for non-existent channel " + + sm.recipientChannelID); + + synchronized (c) + { + if (c.state != Channel.STATE_OPENING) + throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for channel " + + sm.recipientChannelID); + + c.remoteID = sm.senderChannelID; + c.remoteWindow = sm.initialWindowSize & 0xFFFFffffL; /* convert UINT32 to long */ + c.remoteMaxPacketSize = sm.maxPacketSize; + c.state = Channel.STATE_OPEN; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_OPEN_CONFIRMATION (channel " + sm.recipientChannelID + " / remote: " + + sm.senderChannelID + ")"); + } + + public void msgChannelOpenFailure(byte[] msg, int msglen) throws IOException + { + if (msglen < 5) + throw new IOException("SSH_MSG_CHANNEL_OPEN_FAILURE message has wrong size (" + msglen + ")"); + + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + int id = tr.readUINT32(); /* sender channel */ + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE message for non-existent channel " + id); + + int reasonCode = tr.readUINT32(); + String description = tr.readString("UTF-8"); + + String reasonCodeSymbolicName = null; + + switch (reasonCode) + { + case 1: + reasonCodeSymbolicName = "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED"; + break; + case 2: + reasonCodeSymbolicName = "SSH_OPEN_CONNECT_FAILED"; + break; + case 3: + reasonCodeSymbolicName = "SSH_OPEN_UNKNOWN_CHANNEL_TYPE"; + break; + case 4: + reasonCodeSymbolicName = "SSH_OPEN_RESOURCE_SHORTAGE"; + break; + default: + reasonCodeSymbolicName = "UNKNOWN REASON CODE (" + reasonCode + ")"; + } + + StringBuilder descriptionBuffer = new StringBuilder(); + descriptionBuffer.append(description); + + for (int i = 0; i < descriptionBuffer.length(); i++) + { + char cc = descriptionBuffer.charAt(i); + + if ((cc >= 32) && (cc <= 126)) + continue; + descriptionBuffer.setCharAt(i, '\uFFFD'); + } + + synchronized (c) + { + c.EOF = true; + c.state = Channel.STATE_CLOSED; + c.setReasonClosed("The server refused to open the channel (" + reasonCodeSymbolicName + ", '" + + descriptionBuffer.toString() + "')"); + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_OPEN_FAILURE (channel " + id + ")"); + } + + public void msgGlobalRequest(byte[] msg, int msglen) throws IOException + { + /* Currently we do not support any kind of global request */ + + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + String requestName = tr.readString(); + boolean wantReply = tr.readBoolean(); + + if (wantReply) + { + byte[] reply_failure = new byte[1]; + reply_failure[0] = Packets.SSH_MSG_REQUEST_FAILURE; + + tm.sendAsynchronousMessage(reply_failure); + } + + /* We do not clean up the requestName String - that is OK for debug */ + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_GLOBAL_REQUEST (" + requestName + ")"); + } + + public void msgGlobalSuccess() { + synchronized (channels) + { + globalSuccessCounter++; + channels.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_REQUEST_SUCCESS"); + } + + public void msgGlobalFailure() { + synchronized (channels) + { + globalFailedCounter++; + channels.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_REQUEST_FAILURE"); + } + + public void handleMessage(byte[] msg, int msglen) throws IOException + { + if (msg == null) + { + if (log.isEnabled()) + log.log(50, "HandleMessage: got shutdown"); + + synchronized (listenerThreads) + { + for (IChannelWorkerThread lat : listenerThreads) + { + lat.stopWorking(); + } + listenerThreadsAllowed = false; + } + + synchronized (channels) + { + shutdown = true; + + for (Channel c : channels) + { + synchronized (c) + { + c.EOF = true; + c.state = Channel.STATE_CLOSED; + c.setReasonClosed("The connection is being shutdown"); + c.closeMessageRecv = true; /* + * You never know, perhaps + * we are waiting for a + * pending close message + * from the server... + */ + c.notifyAll(); + } + } + /* Works with J2ME */ + channels.clear(); + channels.notifyAll(); /* Notify global response waiters */ + return; + } + } + + switch (msg[0]) + { + case Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION: + msgChannelOpenConfirmation(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST: + msgChannelWindowAdjust(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_DATA: + msgChannelData(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_EXTENDED_DATA: + msgChannelExtendedData(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_REQUEST: + msgChannelRequest(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_EOF: + msgChannelEOF(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_OPEN: + msgChannelOpen(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_CLOSE: + msgChannelClose(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_SUCCESS: + msgChannelSuccess(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_FAILURE: + msgChannelFailure(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_OPEN_FAILURE: + msgChannelOpenFailure(msg, msglen); + break; + case Packets.SSH_MSG_GLOBAL_REQUEST: + msgGlobalRequest(msg, msglen); + break; + case Packets.SSH_MSG_REQUEST_SUCCESS: + msgGlobalSuccess(); + break; + case Packets.SSH_MSG_REQUEST_FAILURE: + msgGlobalFailure(); + break; + default: + throw new IOException("Cannot handle unknown channel message " + (msg[0] & 0xff)); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java new file mode 100644 index 0000000000..ef7acd9eca --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java @@ -0,0 +1,71 @@ +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * ChannelOutputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelOutputStream.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public final class ChannelOutputStream extends OutputStream +{ + Channel c; + + private byte[] writeBuffer; + + boolean isClosed = false; + + ChannelOutputStream(Channel c) + { + this.c = c; + writeBuffer = new byte[1]; + } + + public void write(int b) throws IOException + { + writeBuffer[0] = (byte) b; + + write(writeBuffer, 0, 1); + } + + public void close() throws IOException + { + if (!isClosed) + { + isClosed = true; + c.cm.sendEOF(c); + } + } + + public void flush() throws IOException + { + if (isClosed) + throw new IOException("This OutputStream is closed."); + + /* This is a no-op, since this stream is unbuffered */ + } + + public void write(byte[] b, int off, int len) throws IOException + { + if (isClosed) + throw new IOException("This OutputStream is closed."); + + if (b == null) + throw new NullPointerException(); + + if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) + throw new IndexOutOfBoundsException(); + + if (len == 0) + return; + + c.cm.sendData(c, b, off, len); + } + + public void write(byte[] b) throws IOException + { + write(b, 0, b.length); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java new file mode 100644 index 0000000000..629a70108c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java @@ -0,0 +1,194 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.channel; + +import org.connectbot.simplesocks.Socks5Server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * DynamicAcceptThread. + * + * @author Kenny Root + * @version $Id$ + */ +public class DynamicAcceptThread extends Thread implements IChannelWorkerThread { + private ChannelManager cm; + private ServerSocket ss; + + public DynamicAcceptThread(ChannelManager cm, int local_port) + throws IOException { + this.cm = cm; + + setName("DynamicAcceptThread"); + + ss = new ServerSocket(local_port); + } + + public DynamicAcceptThread(ChannelManager cm, InetSocketAddress localAddress) + throws IOException { + this.cm = cm; + + ss = new ServerSocket(); + ss.bind(localAddress); + } + + @Override + public void run() { + try { + cm.registerThread(this); + } catch (IOException e) { + stopWorking(); + return; + } + + while (true) { + final Socket sock; + try { + sock = ss.accept(); + } catch (IOException e) { + stopWorking(); + return; + } + + DynamicAcceptRunnable dar = new DynamicAcceptRunnable(sock); + Thread t = new Thread(dar); + t.setDaemon(true); + t.start(); + } + } + + @Override + public void stopWorking() { + try { + /* This will lead to an IOException in the ss.accept() call */ + ss.close(); + } catch (IOException ignore) { + } + } + + class DynamicAcceptRunnable implements Runnable { + private static final int idleTimeout = 180000; //3 minutes + + private Socket sock; + private InputStream in; + private OutputStream out; + + public DynamicAcceptRunnable(Socket sock) { + this.sock = sock; + + setName("DynamicAcceptRunnable"); + } + + public void run() { + try { + startSession(); + } catch (IOException ioe) { + try { + sock.close(); + } catch (IOException ignore) { + } + } + } + + private void startSession() throws IOException { + sock.setSoTimeout(idleTimeout); + + in = sock.getInputStream(); + out = sock.getOutputStream(); + Socks5Server server = new Socks5Server(in, out); + try { + if (!server.acceptAuthentication() || !server.readRequest()) { + System.out.println("Could not start SOCKS session"); + return; + } + } catch (IOException ioe) { + server.sendReply(Socks5Server.ResponseCode.GENERAL_FAILURE); + return; + } + + if (server.getCommand() == Socks5Server.Command.CONNECT) { + onConnect(server); + } else { + server.sendReply(Socks5Server.ResponseCode.COMMAND_NOT_SUPPORTED); + } + } + + private void onConnect(Socks5Server server) throws IOException { + final Channel cn; + + String destHost = server.getHostName(); + if (destHost == null) { + destHost = server.getAddress().getHostAddress(); + } + + try { + /* + * This may fail, e.g., if the remote port is closed (in + * optimistic terms: not open yet) + */ + + cn = cm.openDirectTCPIPChannel(destHost, server.getPort(), + "127.0.0.1", 0); + + } catch (IOException e) { + /* + * Try to send a notification back to the client and then close the socket. + */ + try { + server.sendReply(Socks5Server.ResponseCode.GENERAL_FAILURE); + } catch (IOException ignore) { + } + + try { + sock.close(); + } catch (IOException ignore) { + } + + return; + } + + server.sendReply(Socks5Server.ResponseCode.SUCCESS); + + final StreamForwarder r2l = new StreamForwarder(cn, null, sock, cn.stdoutStream, out, "RemoteToLocal"); + final StreamForwarder l2r = new StreamForwarder(cn, r2l, sock, in, cn.stdinStream, "LocalToRemote"); + + r2l.setDaemon(true); + l2r.setDaemon(true); + r2l.start(); + l2r.start(); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java new file mode 100644 index 0000000000..7610b9d2e9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java @@ -0,0 +1,13 @@ + +package com.trilead.ssh2.channel; + +/** + * IChannelWorkerThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: IChannelWorkerThread.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +interface IChannelWorkerThread +{ + void stopWorking(); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java new file mode 100644 index 0000000000..e0b92e0b73 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java @@ -0,0 +1,135 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * LocalAcceptThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: LocalAcceptThread.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class LocalAcceptThread extends Thread implements IChannelWorkerThread +{ + ChannelManager cm; + String host_to_connect; + int port_to_connect; + + final ServerSocket ss; + + public LocalAcceptThread(ChannelManager cm, int local_port, String host_to_connect, int port_to_connect) + throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + ss = new ServerSocket(local_port); + } + + public LocalAcceptThread(ChannelManager cm, InetSocketAddress localAddress, String host_to_connect, + int port_to_connect) throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + ss = new ServerSocket(); + ss.bind(localAddress); + } + + public void run() + { + try + { + cm.registerThread(this); + } + catch (IOException e) + { + stopWorking(); + return; + } + + while (true) + { + Socket s = null; + + try + { + s = ss.accept(); + } + catch (IOException e) + { + stopWorking(); + return; + } + + Channel cn = null; + StreamForwarder r2l = null; + StreamForwarder l2r = null; + + try + { + /* This may fail, e.g., if the remote port is closed (in optimistic terms: not open yet) */ + + cn = cm.openDirectTCPIPChannel(host_to_connect, port_to_connect, s.getInetAddress().getHostAddress(), s + .getPort()); + + } + catch (IOException e) + { + /* Simply close the local socket and wait for the next incoming connection */ + + try + { + s.close(); + } + catch (IOException ignore) + { + } + + continue; + } + + try + { + r2l = new StreamForwarder(cn, null, s, cn.stdoutStream, s.getOutputStream(), "RemoteToLocal"); + l2r = new StreamForwarder(cn, r2l, s, s.getInputStream(), cn.stdinStream, "LocalToRemote"); + } + catch (IOException e) + { + try + { + /* This message is only visible during debugging, since we discard the channel immediatelly */ + cn.cm.closeChannel(cn, "Weird error during creation of StreamForwarder (" + e.getMessage() + ")", + true); + } + catch (IOException ignore) + { + } + + continue; + } + + r2l.setDaemon(true); + l2r.setDaemon(true); + r2l.start(); + l2r.start(); + } + } + + public void stopWorking() + { + try + { + /* This will lead to an IOException in the ss.accept() call */ + ss.close(); + } + catch (IOException e) + { + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java new file mode 100644 index 0000000000..3ad9045231 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java @@ -0,0 +1,103 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.net.Socket; + +import com.trilead.ssh2.log.Logger; + + +/** + * RemoteAcceptThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RemoteAcceptThread.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class RemoteAcceptThread extends Thread +{ + private static final Logger log = Logger.getLogger(RemoteAcceptThread.class); + + Channel c; + + String remoteConnectedAddress; + int remoteConnectedPort; + String remoteOriginatorAddress; + int remoteOriginatorPort; + String targetAddress; + int targetPort; + + Socket s; + + public RemoteAcceptThread(Channel c, String remoteConnectedAddress, int remoteConnectedPort, + String remoteOriginatorAddress, int remoteOriginatorPort, String targetAddress, int targetPort) + { + this.c = c; + this.remoteConnectedAddress = remoteConnectedAddress; + this.remoteConnectedPort = remoteConnectedPort; + this.remoteOriginatorAddress = remoteOriginatorAddress; + this.remoteOriginatorPort = remoteOriginatorPort; + this.targetAddress = targetAddress; + this.targetPort = targetPort; + + if (log.isEnabled()) + log.log(20, "RemoteAcceptThread: " + remoteConnectedAddress + "/" + remoteConnectedPort + ", R: " + + remoteOriginatorAddress + "/" + remoteOriginatorPort); + } + + public void run() + { + try + { + c.cm.sendOpenConfirmation(c); + + s = new Socket(targetAddress, targetPort); + + StreamForwarder r2l = new StreamForwarder(c, null, s, c.getStdoutStream(), s.getOutputStream(), + "RemoteToLocal"); + StreamForwarder l2r = new StreamForwarder(c, null, null, s.getInputStream(), c.getStdinStream(), + "LocalToRemote"); + + /* No need to start two threads, one can be executed in the current thread */ + + r2l.setDaemon(true); + r2l.start(); + l2r.run(); + + while (r2l.isAlive()) + { + try + { + r2l.join(); + } + catch (InterruptedException e) + { + } + } + + /* If the channel is already closed, then this is a no-op */ + + c.cm.closeChannel(c, "EOF on both streams reached.", true); + s.close(); + } + catch (IOException e) + { + log.log(50, "IOException in proxy code: " + e.getMessage()); + + try + { + c.cm.closeChannel(c, "IOException in proxy code (" + e.getMessage() + ")", true); + } + catch (IOException e1) + { + } + try + { + if (s != null) + s.close(); + } + catch (IOException e1) + { + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java new file mode 100644 index 0000000000..fbf4b65413 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java @@ -0,0 +1,17 @@ + +package com.trilead.ssh2.channel; + +/** + * RemoteForwardingData. Data about a requested remote forwarding. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RemoteForwardingData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class RemoteForwardingData +{ + public String bindAddress; + public int bindPort; + + String targetAddress; + int targetPort; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java new file mode 100644 index 0000000000..95667a2d57 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java @@ -0,0 +1,247 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; + +import com.trilead.ssh2.log.Logger; + + +/** + * RemoteX11AcceptThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RemoteX11AcceptThread.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class RemoteX11AcceptThread extends Thread +{ + private static final Logger log = Logger.getLogger(RemoteX11AcceptThread.class); + + Channel c; + + String remoteOriginatorAddress; + int remoteOriginatorPort; + + Socket s; + + public RemoteX11AcceptThread(Channel c, String remoteOriginatorAddress, int remoteOriginatorPort) + { + this.c = c; + this.remoteOriginatorAddress = remoteOriginatorAddress; + this.remoteOriginatorPort = remoteOriginatorPort; + } + + public void run() + { + try + { + /* Send Open Confirmation */ + + c.cm.sendOpenConfirmation(c); + + /* Read startup packet from client */ + + OutputStream remote_os = c.getStdinStream(); + InputStream remote_is = c.getStdoutStream(); + + /* The following code is based on the protocol description given in: + * Scheifler/Gettys, + * X Windows System: Core and Extension Protocols: + * X Version 11, Releases 6 and 6.1 ISBN 1-55558-148-X + */ + + /* + * Client startup: + * + * 1 0X42 MSB first/0x6c lSB first - byteorder + * 1 - unused + * 2 card16 - protocol-major-version + * 2 card16 - protocol-minor-version + * 2 n - lenght of authorization-protocol-name + * 2 d - lenght of authorization-protocol-data + * 2 - unused + * string8 - authorization-protocol-name + * p - unused, p=pad(n) + * string8 - authorization-protocol-data + * q - unused, q=pad(d) + * + * pad(X) = (4 - (X mod 4)) mod 4 + * + * Server response: + * + * 1 (0 failed, 2 authenticate, 1 success) + * ... + * + */ + + /* Later on we will simply forward the first 6 header bytes to the "real" X11 server */ + + byte[] header = new byte[6]; + + if (remote_is.read(header) != 6) + throw new IOException("Unexpected EOF on X11 startup!"); + + if ((header[0] != 0x42) && (header[0] != 0x6c)) // 0x42 MSB first, 0x6C LSB first + throw new IOException("Unknown endian format in X11 message!"); + + /* Yes, I came up with this myself - shall I file an application for a patent? =) */ + + int idxMSB = (header[0] == 0x42) ? 0 : 1; + + /* Read authorization data header */ + + byte[] auth_buff = new byte[6]; + + if (remote_is.read(auth_buff) != 6) + throw new IOException("Unexpected EOF on X11 startup!"); + + int authProtocolNameLength = ((auth_buff[idxMSB] & 0xff) << 8) | (auth_buff[1 - idxMSB] & 0xff); + int authProtocolDataLength = ((auth_buff[2 + idxMSB] & 0xff) << 8) | (auth_buff[3 - idxMSB] & 0xff); + + if ((authProtocolNameLength > 256) || (authProtocolDataLength > 256)) + throw new IOException("Buggy X11 authorization data"); + + int authProtocolNamePadding = ((4 - (authProtocolNameLength % 4)) % 4); + int authProtocolDataPadding = ((4 - (authProtocolDataLength % 4)) % 4); + + byte[] authProtocolName = new byte[authProtocolNameLength]; + byte[] authProtocolData = new byte[authProtocolDataLength]; + + byte[] paddingBuffer = new byte[4]; + + if (remote_is.read(authProtocolName) != authProtocolNameLength) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolName)"); + + if (remote_is.read(paddingBuffer, 0, authProtocolNamePadding) != authProtocolNamePadding) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolNamePadding)"); + + if (remote_is.read(authProtocolData) != authProtocolDataLength) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolData)"); + + if (remote_is.read(paddingBuffer, 0, authProtocolDataPadding) != authProtocolDataPadding) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolDataPadding)"); + + String authProtocolNameStr; + try { + authProtocolNameStr = new String(authProtocolName, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + authProtocolNameStr = new String(authProtocolName); + } + if (!"MIT-MAGIC-COOKIE-1".equals(authProtocolNameStr)) + throw new IOException("Unknown X11 authorization protocol!"); + + if (authProtocolDataLength != 16) + throw new IOException("Wrong data length for X11 authorization data!"); + + StringBuffer tmp = new StringBuffer(32); + for (int i = 0; i < authProtocolData.length; i++) + { + String digit2 = Integer.toHexString(authProtocolData[i] & 0xff); + tmp.append((digit2.length() == 2) ? digit2 : "0" + digit2); + } + String hexEncodedFakeCookie = tmp.toString(); + + /* Order is very important here - it may be that a certain x11 forwarding + * gets disabled right in the moment when we check and register our connection + * */ + + synchronized (c) + { + /* Please read the comment in Channel.java */ + c.hexX11FakeCookie = hexEncodedFakeCookie; + } + + /* Now check our fake cookie directory to see if we produced this cookie */ + + X11ServerData sd = c.cm.checkX11Cookie(hexEncodedFakeCookie); + + if (sd == null) + throw new IOException("Invalid X11 cookie received."); + + /* If the session which corresponds to this cookie is closed then we will + * detect this: the session's close code will close all channels + * with the session's assigned x11 fake cookie. + */ + + s = new Socket(sd.hostname, sd.port); + + OutputStream x11_os = s.getOutputStream(); + InputStream x11_is = s.getInputStream(); + + /* Now we are sending the startup packet to the real X11 server */ + + x11_os.write(header); + + if (sd.x11_magic_cookie == null) + { + byte[] emptyAuthData = new byte[6]; + /* empty auth data, hopefully you are connecting to localhost =) */ + x11_os.write(emptyAuthData); + } + else + { + if (sd.x11_magic_cookie.length != 16) + throw new IOException("The real X11 cookie has an invalid length!"); + + /* send X11 cookie specified by client */ + x11_os.write(auth_buff); + x11_os.write(authProtocolName); /* re-use */ + x11_os.write(paddingBuffer, 0, authProtocolNamePadding); + x11_os.write(sd.x11_magic_cookie); + x11_os.write(paddingBuffer, 0, authProtocolDataPadding); + } + + x11_os.flush(); + + /* Start forwarding traffic */ + + StreamForwarder r2l = new StreamForwarder(c, null, s, remote_is, x11_os, "RemoteToX11"); + StreamForwarder l2r = new StreamForwarder(c, null, null, x11_is, remote_os, "X11ToRemote"); + + /* No need to start two threads, one can be executed in the current thread */ + + r2l.setDaemon(true); + r2l.start(); + l2r.run(); + + while (r2l.isAlive()) + { + try + { + r2l.join(); + } + catch (InterruptedException e) + { + } + } + + /* If the channel is already closed, then this is a no-op */ + + c.cm.closeChannel(c, "EOF on both X11 streams reached.", true); + s.close(); + } + catch (IOException e) + { + log.log(50, "IOException in X11 proxy code: " + e.getMessage()); + + try + { + c.cm.closeChannel(c, "IOException in X11 proxy code (" + e.getMessage() + ")", true); + } + catch (IOException e1) + { + } + try + { + if (s != null) + s.close(); + } + catch (IOException e1) + { + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java new file mode 100644 index 0000000000..496b5736a0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java @@ -0,0 +1,111 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +/** + * A StreamForwarder forwards data between two given streams. + * If two StreamForwarder threads are used (one for each direction) + * then one can be configured to shutdown the underlying channel/socket + * if both threads have finished forwarding (EOF). + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: StreamForwarder.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class StreamForwarder extends Thread +{ + final OutputStream os; + final InputStream is; + final byte[] buffer = new byte[Channel.CHANNEL_BUFFER_SIZE]; + final Channel c; + final StreamForwarder sibling; + final Socket s; + final String mode; + + StreamForwarder(Channel c, StreamForwarder sibling, Socket s, InputStream is, OutputStream os, String mode) { + this.is = is; + this.os = os; + this.mode = mode; + this.c = c; + this.sibling = sibling; + this.s = s; + } + + public void run() + { + try + { + while (true) + { + int len = is.read(buffer); + if (len <= 0) + break; + os.write(buffer, 0, len); + os.flush(); + } + } + catch (IOException ignore) + { + try + { + c.cm.closeChannel(c, "Closed due to exception in StreamForwarder (" + mode + "): " + + ignore.getMessage(), true); + } + catch (IOException e) + { + } + } + finally + { + try + { + os.close(); + } + catch (IOException e1) + { + } + try + { + is.close(); + } + catch (IOException e2) + { + } + + if (sibling != null) + { + while (sibling.isAlive()) + { + try + { + sibling.join(); + } + catch (InterruptedException e) + { + } + } + + try + { + c.cm.closeChannel(c, "StreamForwarder (" + mode + ") is cleaning up the connection", true); + } + catch (IOException e3) + { + } + } + + if (s != null) { + try + { + s.close(); + } + catch (IOException e1) + { + } + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/X11ServerData.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/X11ServerData.java new file mode 100644 index 0000000000..2c85b98ba8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/channel/X11ServerData.java @@ -0,0 +1,16 @@ + +package com.trilead.ssh2.channel; + +/** + * X11ServerData. Data regarding an x11 forwarding target. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: X11ServerData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + * + */ +public class X11ServerData +{ + public String hostname; + public int port; + public byte[] x11_magic_cookie; /* not the remote (fake) one, the local (real) one */ +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java new file mode 100644 index 0000000000..153a74f56d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java @@ -0,0 +1,113 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.compression; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Kenny Root + * + */ +public class CompressionFactory { + private CompressionFactory() { } + + private static class CompressorEntry + { + String type; + String compressorClass; + + private CompressorEntry(String type, String compressorClass) + { + this.type = type; + this.compressorClass = compressorClass; + } + } + + private static List compressors = new ArrayList<>(); + + static + { + /* Higher Priority First */ + compressors.add(new CompressorEntry("zlib", "com.trilead.ssh2.compression.Zlib")); + compressors.add(new CompressorEntry("zlib@openssh.com", "com.trilead.ssh2.compression.ZlibOpenSSH")); + compressors.add(new CompressorEntry("none", "")); + } + + static void addCompressor(String protocolName, String className) { + compressors.add(new CompressorEntry(protocolName, className)); + } + + public static String[] getDefaultCompressorList() + { + String[] list = new String[compressors.size()]; + for (int i = 0; i < compressors.size(); i++) + { + CompressorEntry ce = compressors.get(i); + list[i] = ce.type; + } + return list; + } + + public static void checkCompressorList(String[] compressorCandidates) + { + for (String compressorCandidate : compressorCandidates) { + getEntry(compressorCandidate); + } + } + + public static ICompressor createCompressor(String type) + { + try + { + CompressorEntry ce = getEntry(type); + if ("".equals(ce.compressorClass)) + return null; + + Class cc = Class.forName(ce.compressorClass); + Constructor constructor = cc.getConstructor(); + return (ICompressor) constructor.newInstance(); + } + catch (Exception e) + { + throw new IllegalArgumentException("Cannot instantiate " + type); + } + } + + private static CompressorEntry getEntry(String type) + { + for (CompressorEntry ce : compressors) { + if (ce.type.equals(type)) + return ce; + } + throw new IllegalArgumentException("Unknown algorithm " + type); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ICompressor.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ICompressor.java new file mode 100644 index 0000000000..5b733ec94c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ICompressor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.compression; + +/** + * @author Kenny Root + * + */ +public interface ICompressor { + int getBufferSize(); + + int compress(byte[] buf, int start, int len, byte[] output); + + byte[] uncompress(byte[] buf, int start, int[] len); + + boolean canCompressPreauth(); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/Zlib.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/Zlib.java new file mode 100644 index 0000000000..c1acab3f6c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/Zlib.java @@ -0,0 +1,142 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.compression; + +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZStream; + +/** + * @author Kenny Root + * + */ +public class Zlib implements ICompressor { + static private final int DEFAULT_BUF_SIZE = 4096; + static private final int LEVEL = 5; + + private ZStream deflate; + private byte[] deflate_tmpbuf; + + private ZStream inflate; + private byte[] inflate_tmpbuf; + private byte[] inflated_buf; + + public Zlib() { + deflate = new ZStream(); + inflate = new ZStream(); + + deflate.deflateInit(LEVEL); + inflate.inflateInit(); + + deflate_tmpbuf = new byte[DEFAULT_BUF_SIZE]; + inflate_tmpbuf = new byte[DEFAULT_BUF_SIZE]; + inflated_buf = new byte[DEFAULT_BUF_SIZE]; + } + + public boolean canCompressPreauth() { + return true; + } + + public int getBufferSize() { + return DEFAULT_BUF_SIZE; + } + + public int compress(byte[] buf, int start, int len, byte[] output) { + deflate.next_in = buf; + deflate.next_in_index = start; + deflate.avail_in = len - start; + + if ((buf.length + 1024) > deflate_tmpbuf.length) { + deflate_tmpbuf = new byte[buf.length + 1024]; + } + + deflate.next_out = deflate_tmpbuf; + deflate.next_out_index = 0; + deflate.avail_out = output.length; + + if (deflate.deflate(JZlib.Z_PARTIAL_FLUSH) != JZlib.Z_OK) { + System.err.println("compress: compression failure"); + } + + if (deflate.avail_in > 0) { + System.err.println("compress: deflated data too large"); + } + + int outputlen = output.length - deflate.avail_out; + + System.arraycopy(deflate_tmpbuf, 0, output, 0, outputlen); + + return outputlen; + } + + public byte[] uncompress(byte[] buffer, int start, int[] length) { + int inflated_end = 0; + + inflate.next_in = buffer; + inflate.next_in_index = start; + inflate.avail_in = length[0]; + + while (true) { + inflate.next_out = inflate_tmpbuf; + inflate.next_out_index = 0; + inflate.avail_out = DEFAULT_BUF_SIZE; + int status = inflate.inflate(JZlib.Z_PARTIAL_FLUSH); + switch (status) { + case JZlib.Z_OK: + if (inflated_buf.length < inflated_end + DEFAULT_BUF_SIZE + - inflate.avail_out) { + byte[] foo = new byte[inflated_end + DEFAULT_BUF_SIZE + - inflate.avail_out]; + System.arraycopy(inflated_buf, 0, foo, 0, inflated_end); + inflated_buf = foo; + } + System.arraycopy(inflate_tmpbuf, 0, inflated_buf, inflated_end, + DEFAULT_BUF_SIZE - inflate.avail_out); + inflated_end += (DEFAULT_BUF_SIZE - inflate.avail_out); + length[0] = inflated_end; + break; + case JZlib.Z_BUF_ERROR: + if (inflated_end > buffer.length - start) { + byte[] foo = new byte[inflated_end + start]; + System.arraycopy(buffer, 0, foo, 0, start); + System.arraycopy(inflated_buf, 0, foo, start, inflated_end); + buffer = foo; + } else { + System.arraycopy(inflated_buf, 0, buffer, start, + inflated_end); + } + length[0] = inflated_end; + return buffer; + default: + System.err.println("uncompress: inflate returnd " + status); + return null; + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java new file mode 100644 index 0000000000..039766a849 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java @@ -0,0 +1,47 @@ +/* + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.compression; + +/** + * Defines how zlib@openssh.org compression works. + * See + * http://www.openssh.org/txt/draft-miller-secsh-compression-delayed-00.txt + * compression is disabled until userauth has occurred. + * + * @author Matt Johnston + * + */ +public class ZlibOpenSSH extends Zlib { + + public boolean canCompressPreauth() { + return false; + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/Base64.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/Base64.java new file mode 100644 index 0000000000..096f6734af --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/Base64.java @@ -0,0 +1,148 @@ + +package com.trilead.ssh2.crypto; + +import java.io.CharArrayWriter; +import java.io.IOException; + +/** + * Basic Base64 Support. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Base64.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class Base64 +{ + static final char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + + public static char[] encode(byte[] content) + { + CharArrayWriter cw = new CharArrayWriter((4 * content.length) / 3); + + int idx = 0; + + int x = 0; + + for (int i = 0; i < content.length; i++) + { + if (idx == 0) + x = (content[i] & 0xff) << 16; + else if (idx == 1) + x = x | ((content[i] & 0xff) << 8); + else + x = x | (content[i] & 0xff); + + idx++; + + if (idx == 3) + { + cw.write(alphabet[x >> 18]); + cw.write(alphabet[(x >> 12) & 0x3f]); + cw.write(alphabet[(x >> 6) & 0x3f]); + cw.write(alphabet[x & 0x3f]); + + idx = 0; + } + } + + if (idx == 1) + { + cw.write(alphabet[x >> 18]); + cw.write(alphabet[(x >> 12) & 0x3f]); + cw.write('='); + cw.write('='); + } + + if (idx == 2) + { + cw.write(alphabet[x >> 18]); + cw.write(alphabet[(x >> 12) & 0x3f]); + cw.write(alphabet[(x >> 6) & 0x3f]); + cw.write('='); + } + + return cw.toCharArray(); + } + + public static byte[] decode(char[] message) throws IOException + { + byte buff[] = new byte[4]; + byte dest[] = new byte[message.length]; + + int bpos = 0; + int destpos = 0; + + for (int i = 0; i < message.length; i++) + { + int c = message[i]; + + if ((c == '\n') || (c == '\r') || (c == ' ') || (c == '\t')) + continue; + + if ((c >= 'A') && (c <= 'Z')) + { + buff[bpos++] = (byte) (c - 'A'); + } + else if ((c >= 'a') && (c <= 'z')) + { + buff[bpos++] = (byte) ((c - 'a') + 26); + } + else if ((c >= '0') && (c <= '9')) + { + buff[bpos++] = (byte) ((c - '0') + 52); + } + else if (c == '+') + { + buff[bpos++] = 62; + } + else if (c == '/') + { + buff[bpos++] = 63; + } + else if (c == '=') + { + buff[bpos++] = 64; + } + else + { + throw new IOException("Illegal char in base64 code."); + } + + if (bpos == 4) + { + bpos = 0; + + if (buff[0] == 64) + break; + + if (buff[1] == 64) + throw new IOException("Unexpected '=' in base64 code."); + + if (buff[2] == 64) + { + int v = (((buff[0] & 0x3f) << 6) | ((buff[1] & 0x3f))); + dest[destpos++] = (byte) (v >> 4); + break; + } + else if (buff[3] == 64) + { + int v = (((buff[0] & 0x3f) << 12) | ((buff[1] & 0x3f) << 6) | ((buff[2] & 0x3f))); + dest[destpos++] = (byte) (v >> 10); + dest[destpos++] = (byte) (v >> 2); + break; + } + else + { + int v = (((buff[0] & 0x3f) << 18) | ((buff[1] & 0x3f) << 12) | ((buff[2] & 0x3f) << 6) | ((buff[3] & 0x3f))); + dest[destpos++] = (byte) (v >> 16); + dest[destpos++] = (byte) (v >> 8); + dest[destpos++] = (byte) (v); + } + } + } + + byte[] res = new byte[destpos]; + System.arraycopy(dest, 0, res, 0, destpos); + + return res; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java new file mode 100644 index 0000000000..f080c8d0db --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java @@ -0,0 +1,26 @@ + +package com.trilead.ssh2.crypto; + +import com.trilead.ssh2.compression.CompressionFactory; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.digest.MACs; +import com.trilead.ssh2.transport.KexManager; + + +/** + * CryptoWishList. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CryptoWishList.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class CryptoWishList +{ + public String[] kexAlgorithms = KexManager.getDefaultKexAlgorithmList(); + public String[] serverHostKeyAlgorithms = KexManager.getDefaultServerHostkeyAlgorithmList(); + public String[] c2s_enc_algos = BlockCipherFactory.getDefaultCipherList(); + public String[] s2c_enc_algos = BlockCipherFactory.getDefaultCipherList(); + public String[] c2s_mac_algos = MACs.getMacList(); + public String[] s2c_mac_algos = MACs.getMacList(); + public String[] c2s_comp_algos = CompressionFactory.getDefaultCompressorList(); + public String[] s2c_comp_algos = CompressionFactory.getDefaultCompressorList(); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java new file mode 100644 index 0000000000..e711f5b7ca --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java @@ -0,0 +1,91 @@ + +package com.trilead.ssh2.crypto; + + +import java.math.BigInteger; + +import com.trilead.ssh2.crypto.digest.HashForSSH2Types; + +/** + * Establishes key material for iv/key/mac (both directions). + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KeyMaterial.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KeyMaterial +{ + public byte[] initial_iv_client_to_server; + public byte[] initial_iv_server_to_client; + public byte[] enc_key_client_to_server; + public byte[] enc_key_server_to_client; + public byte[] integrity_key_client_to_server; + public byte[] integrity_key_server_to_client; + + private static byte[] calculateKey(HashForSSH2Types sh, BigInteger K, byte[] H, byte type, byte[] SessionID, + int keyLength) + { + byte[] res = new byte[keyLength]; + + int dglen = sh.getDigestLength(); + int numRounds = (keyLength + dglen - 1) / dglen; + + byte[][] tmp = new byte[numRounds][]; + + sh.reset(); + sh.updateBigInt(K); + sh.updateBytes(H); + sh.updateByte(type); + sh.updateBytes(SessionID); + + tmp[0] = sh.getDigest(); + + int off = 0; + int produced = Math.min(dglen, keyLength); + + System.arraycopy(tmp[0], 0, res, off, produced); + + keyLength -= produced; + off += produced; + + for (int i = 1; i < numRounds; i++) + { + sh.updateBigInt(K); + sh.updateBytes(H); + + for (int j = 0; j < i; j++) + sh.updateBytes(tmp[j]); + + tmp[i] = sh.getDigest(); + + produced = Math.min(dglen, keyLength); + System.arraycopy(tmp[i], 0, res, off, produced); + keyLength -= produced; + off += produced; + } + + return res; + } + + public static KeyMaterial create(String hashAlgo, byte[] H, BigInteger K, byte[] SessionID, int keyLengthCS, + int blockSizeCS, int macLengthCS, int keyLengthSC, int blockSizeSC, int macLengthSC) + throws IllegalArgumentException + { + KeyMaterial km = new KeyMaterial(); + + HashForSSH2Types sh = new HashForSSH2Types(hashAlgo); + + km.initial_iv_client_to_server = calculateKey(sh, K, H, (byte) 'A', SessionID, blockSizeCS); + + km.initial_iv_server_to_client = calculateKey(sh, K, H, (byte) 'B', SessionID, blockSizeSC); + + km.enc_key_client_to_server = calculateKey(sh, K, H, (byte) 'C', SessionID, keyLengthCS); + + km.enc_key_server_to_client = calculateKey(sh, K, H, (byte) 'D', SessionID, keyLengthSC); + + km.integrity_key_client_to_server = calculateKey(sh, K, H, (byte) 'E', SessionID, macLengthCS); + + km.integrity_key_server_to_client = calculateKey(sh, K, H, (byte) 'F', SessionID, macLengthSC); + + return km; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java new file mode 100644 index 0000000000..cc0c2f4f54 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java @@ -0,0 +1,696 @@ + +package com.trilead.ssh2.crypto; + +import java.io.BufferedReader; +import java.io.CharArrayReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.DigestException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import java.util.Locale; + +import com.trilead.ssh2.crypto.cipher.AES; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.DES; +import com.trilead.ssh2.crypto.cipher.DESede; +import com.trilead.ssh2.crypto.keys.Ed25519PrivateKey; +import com.trilead.ssh2.crypto.keys.Ed25519PublicKey; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.Ed25519Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; +import org.mindrot.jbcrypt.BCrypt; + +/** + * PEM Support. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PEMDecoder.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class PEMDecoder +{ + public static final int PEM_RSA_PRIVATE_KEY = 1; + public static final int PEM_DSA_PRIVATE_KEY = 2; + public static final int PEM_EC_PRIVATE_KEY = 3; + public static final int PEM_OPENSSH_PRIVATE_KEY = 4; + + private static final byte[] OPENSSH_V1_MAGIC = new byte[] { + 'o', 'p', 'e', 'n', 's', 's', 'h', '-', 'k', 'e', 'y', '-', 'v', '1', '\0', + }; + + private static int hexToInt(char c) + { + if ((c >= 'a') && (c <= 'f')) + { + return (c - 'a') + 10; + } + + if ((c >= 'A') && (c <= 'F')) + { + return (c - 'A') + 10; + } + + if ((c >= '0') && (c <= '9')) + { + return (c - '0'); + } + + throw new IllegalArgumentException("Need hex char"); + } + + private static byte[] hexToByteArray(String hex) + { + if (hex == null) + throw new IllegalArgumentException("null argument"); + + if ((hex.length() % 2) != 0) + throw new IllegalArgumentException("Uneven string length in hex encoding."); + + byte decoded[] = new byte[hex.length() / 2]; + + for (int i = 0; i < decoded.length; i++) + { + int hi = hexToInt(hex.charAt(i * 2)); + int lo = hexToInt(hex.charAt((i * 2) + 1)); + + decoded[i] = (byte) (hi * 16 + lo); + } + + return decoded; + } + + private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen) + throws IOException + { + if (salt.length < 8) + throw new IllegalArgumentException("Salt needs to be at least 8 bytes for key generation."); + + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("VM does not support MD5", e); + } + + byte[] key = new byte[keyLen]; + byte[] tmp = new byte[md5.getDigestLength()]; + + while (true) + { + md5.update(password, 0, password.length); + md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the + // salt in this step. + // This took me two hours until I got AES-xxx running. + + int copy = (keyLen < tmp.length) ? keyLen : tmp.length; + + try { + md5.digest(tmp, 0, tmp.length); + } catch (DigestException e) { + throw new IOException("could not digest password", e); + } + + System.arraycopy(tmp, 0, key, key.length - keyLen, copy); + + keyLen -= copy; + + if (keyLen == 0) + return key; + + md5.update(tmp, 0, tmp.length); + } + } + + private static byte[] removePadding(byte[] buff, int blockSize) throws IOException + { + /* Removes RFC 1423/PKCS #7 padding */ + + int rfc_1423_padding = buff[buff.length - 1] & 0xff; + + if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize)) + throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); + + for (int i = 2; i <= rfc_1423_padding; i++) + { + if (buff[buff.length - i] != rfc_1423_padding) + throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); + } + + byte[] tmp = new byte[buff.length - rfc_1423_padding]; + System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding); + return tmp; + } + + public static final PEMStructure parsePEM(char[] pem) throws IOException + { + PEMStructure ps = new PEMStructure(); + + String line = null; + + BufferedReader br = new BufferedReader(new CharArrayReader(pem)); + + String endLine = null; + + while (true) + { + line = br.readLine(); + + if (line == null) + throw new IOException("Invalid PEM structure, '-----BEGIN...' missing"); + + line = line.trim(); + + if (line.startsWith("-----BEGIN DSA PRIVATE KEY-----")) + { + endLine = "-----END DSA PRIVATE KEY-----"; + ps.pemType = PEM_DSA_PRIVATE_KEY; + break; + } + + if (line.startsWith("-----BEGIN RSA PRIVATE KEY-----")) + { + endLine = "-----END RSA PRIVATE KEY-----"; + ps.pemType = PEM_RSA_PRIVATE_KEY; + break; + } + + if (line.startsWith("-----BEGIN EC PRIVATE KEY-----")) { + endLine = "-----END EC PRIVATE KEY-----"; + ps.pemType = PEM_EC_PRIVATE_KEY; + break; + } + + if (line.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----")) { + endLine = "-----END OPENSSH PRIVATE KEY-----"; + ps.pemType = PEM_OPENSSH_PRIVATE_KEY; + break; + } + } + + while (true) + { + line = br.readLine(); + + if (line == null) + throw new IOException("Invalid PEM structure, " + endLine + " missing"); + + line = line.trim(); + + int sem_idx = line.indexOf(':'); + + if (sem_idx == -1) + break; + + String name = line.substring(0, sem_idx + 1); + String value = line.substring(sem_idx + 1); + + String values[] = value.split(","); + + for (int i = 0; i < values.length; i++) + values[i] = values[i].trim(); + + // Proc-Type: 4,ENCRYPTED + // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483 + + if ("Proc-Type:".equals(name)) + { + ps.procType = values; + continue; + } + + if ("DEK-Info:".equals(name)) + { + ps.dekInfo = values; + continue; + } + /* Ignore line */ + } + + StringBuffer keyData = new StringBuffer(); + + while (true) + { + if (line == null) + throw new IOException("Invalid PEM structure, " + endLine + " missing"); + + line = line.trim(); + + if (line.startsWith(endLine)) + break; + + keyData.append(line); + + line = br.readLine(); + } + + char[] pem_chars = new char[keyData.length()]; + keyData.getChars(0, pem_chars.length, pem_chars, 0); + + ps.data = Base64.decode(pem_chars); + + if (ps.data.length == 0) + throw new IOException("Invalid PEM structure, no data available"); + + return ps; + } + + private static byte[] decryptData(byte[] data, byte[] pw, byte[] salt, int rounds, String algo) throws IOException + { + BlockCipher bc; + int keySize; + + String algoLower = algo.toLowerCase(Locale.US); + if (algoLower.equals("des-ede3-cbc")) + { + bc = new DESede.CBC(); + keySize = 24; + } + else if (algoLower.equals("des-cbc")) + { + bc = new DES.CBC(); + keySize = 8; + } + else if (algoLower.equals("aes-128-cbc") || algoLower.equals("aes128-cbc")) + { + bc = new AES.CBC(); + keySize = 16; + } + else if (algoLower.equals("aes-192-cbc") || algoLower.equals("aes192-cbc")) + { + bc = new AES.CBC(); + keySize = 24; + } + else if (algoLower.equals("aes-256-cbc") || algoLower.equals("aes256-cbc")) + { + bc = new AES.CBC(); + keySize = 32; + } + else if (algoLower.equals("aes-128-ctr") || algoLower.equals("aes128-ctr")) + { + bc = new AES.CTR(); + keySize = 16; + } + else if (algoLower.equals("aes-192-ctr") || algoLower.equals("aes192-ctr")) + { + bc = new AES.CTR(); + keySize = 24; + } + else if (algoLower.equals("aes-256-ctr") || algoLower.equals("aes256-ctr")) + { + bc = new AES.CTR(); + keySize = 32; + } + else + { + throw new IOException("Cannot decrypt PEM structure, unknown cipher " + algo); + } + + if (rounds == -1) + { + bc.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, keySize), salt); + } + else + { + byte[] key = new byte[keySize]; + byte[] iv = new byte[bc.getBlockSize()]; + + byte[] keyAndIV = new byte[key.length + iv.length]; + + new BCrypt().pbkdf(pw, salt, rounds, keyAndIV); + + System.arraycopy(keyAndIV, 0, key, 0, key.length); + System.arraycopy(keyAndIV, key.length, iv, 0, iv.length); + + bc.init(false, key, iv); + } + + + if ((data.length % bc.getBlockSize()) != 0) + throw new IOException("Invalid PEM structure, size of encrypted block is not a multiple of " + + bc.getBlockSize()); + + /* Now decrypt the content */ + byte[] dz = new byte[data.length]; + + for (int i = 0; i < data.length / bc.getBlockSize(); i++) + { + bc.transformBlock(data, i * bc.getBlockSize(), dz, i * bc.getBlockSize()); + } + + if (rounds == -1) { + /* Now check and remove RFC 1423/PKCS #7 padding */ + return removePadding(dz, bc.getBlockSize()); + } else { + /* New style is to check the padding after reading the comment. */ + return dz; + } + } + + private static void decryptPEM(PEMStructure ps, byte[] pw) throws IOException + { + if (ps.dekInfo == null) + throw new IOException("Broken PEM, no mode and salt given, but encryption enabled"); + + if (ps.dekInfo.length != 2) + throw new IOException("Broken PEM, DEK-Info is incomplete!"); + + String algo = ps.dekInfo[0]; + byte[] salt = hexToByteArray(ps.dekInfo[1]); + + byte[] dz = decryptData(ps.data, pw, salt, -1, algo); + + ps.data = dz; + ps.dekInfo = null; + ps.procType = null; + } + + public static final boolean isPEMEncrypted(PEMStructure ps) throws IOException + { + if (ps.pemType == PEM_OPENSSH_PRIVATE_KEY) { + TypesReader tr = new TypesReader(ps.data); + byte[] magic = tr.readBytes(OPENSSH_V1_MAGIC.length); + if (!Arrays.equals(OPENSSH_V1_MAGIC, magic)) { + throw new IOException("Could not find OPENSSH key magic: " + new String(magic)); + } + + tr.readString(); + String kdfname = tr.readString(); + return !"none".equals(kdfname); + } + + if (ps.procType == null) + return false; + + if (ps.procType.length != 2) + throw new IOException("Unknown Proc-Type field."); + + if (!"4".equals(ps.procType[0])) + throw new IOException("Unknown Proc-Type field (" + ps.procType[0] + ")"); + + return "ENCRYPTED".equals(ps.procType[1]); + + } + + public static KeyPair decode(char[] pem, String password) throws IOException + { + PEMStructure ps = parsePEM(pem); + return decode(ps, password); + } + + public static KeyPair decode(PEMStructure ps, String password) throws IOException + { + if (isPEMEncrypted(ps) && ps.pemType != PEM_OPENSSH_PRIVATE_KEY) + { + if (password == null) + throw new IOException("PEM is encrypted, but no password was specified"); + + try { + decryptPEM(ps, password.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + decryptPEM(ps, password.getBytes("ISO-8859-1")); + } + } + + if (ps.pemType == PEM_DSA_PRIVATE_KEY) + { + SimpleDERReader dr = new SimpleDERReader(ps.data); + + byte[] seq = dr.readSequenceAsByteArray(); + + if (dr.available() != 0) + throw new IOException("Padding in DSA PRIVATE KEY DER stream."); + + dr.resetInput(seq); + + BigInteger version = dr.readInt(); + + if (version.compareTo(BigInteger.ZERO) != 0) + throw new IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream."); + + BigInteger p = dr.readInt(); + BigInteger q = dr.readInt(); + BigInteger g = dr.readInt(); + BigInteger y = dr.readInt(); + BigInteger x = dr.readInt(); + + if (dr.available() != 0) + throw new IOException("Padding in DSA PRIVATE KEY DER stream."); + + DSAPrivateKeySpec privSpec = new DSAPrivateKeySpec(x, p, q, g); + DSAPublicKeySpec pubSpec = new DSAPublicKeySpec(y, p, q, g); + + return generateKeyPair("DSA", privSpec, pubSpec); + } + + if (ps.pemType == PEM_RSA_PRIVATE_KEY) + { + SimpleDERReader dr = new SimpleDERReader(ps.data); + + byte[] seq = dr.readSequenceAsByteArray(); + + if (dr.available() != 0) + throw new IOException("Padding in RSA PRIVATE KEY DER stream."); + + dr.resetInput(seq); + + BigInteger version = dr.readInt(); + + if ((version.compareTo(BigInteger.ZERO) != 0) && (version.compareTo(BigInteger.ONE) != 0)) + throw new IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream."); + + BigInteger n = dr.readInt(); + BigInteger e = dr.readInt(); + BigInteger d = dr.readInt(); + // TODO: is this right? + BigInteger primeP = dr.readInt(); + BigInteger primeQ = dr.readInt(); + BigInteger expP = dr.readInt(); + BigInteger expQ = dr.readInt(); + BigInteger coeff = dr.readInt(); + + RSAPrivateKeySpec privSpec = new RSAPrivateCrtKeySpec(n, e, d, primeP, primeQ, expP, expQ, coeff); + RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(n, e); + + return generateKeyPair("RSA", privSpec, pubSpec); + } + + if (ps.pemType == PEM_EC_PRIVATE_KEY) { + SimpleDERReader dr = new SimpleDERReader(ps.data); + + byte[] seq = dr.readSequenceAsByteArray(); + + if (dr.available() != 0) + throw new IOException("Padding in EC PRIVATE KEY DER stream."); + + dr.resetInput(seq); + + BigInteger version = dr.readInt(); + + if ((version.compareTo(BigInteger.ONE) != 0)) + throw new IOException("Wrong version (" + version + ") in EC PRIVATE KEY DER stream."); + + byte[] privateBytes = dr.readOctetString(); + + String curveOid = null; + byte[] publicBytes = null; + while (dr.available() > 0) { + int type = dr.readConstructedType(); + SimpleDERReader cr = dr.readConstructed(); + switch (type) { + case 0: + curveOid = cr.readOid(); + break; + case 1: + publicBytes = cr.readOctetString(); + break; + } + } + + ECDSASHA2Verify verifier = ECDSASHA2Verify.getVerifierForOID(curveOid); + if (verifier == null) + throw new IOException("invalid OID"); + + BigInteger s = new BigInteger(1, privateBytes); + byte[] publicBytesSlice = new byte[publicBytes.length - 1]; + System.arraycopy(publicBytes, 1, publicBytesSlice, 0, publicBytesSlice.length); + ECParameterSpec params = verifier.getParameterSpec(); + ECPoint w = verifier.decodeECPoint(publicBytesSlice); + + ECPrivateKeySpec privSpec = new ECPrivateKeySpec(s, params); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, params); + + return generateKeyPair("EC", privSpec, pubSpec); + } + + if (ps.pemType == PEM_OPENSSH_PRIVATE_KEY) { + TypesReader tr = new TypesReader(ps.data); + byte[] magic = tr.readBytes(OPENSSH_V1_MAGIC.length); + if (!Arrays.equals(OPENSSH_V1_MAGIC, magic)) { + throw new IOException("Could not find OPENSSH key magic: " + new String(magic)); + } + + String ciphername = tr.readString(); + String kdfname = tr.readString(); + byte[] kdfoptions = tr.readByteString(); + int numberOfKeys = tr.readUINT32(); + + // TODO support multiple keys + if (numberOfKeys != 1) { + throw new IOException("Only one key supported, but encountered bundle of " + numberOfKeys); + } + + // OpenSSH discards this, so we will as well. + tr.readByteString(); + + byte[] dataBytes = tr.readByteString(); + + if ("bcrypt".equals(kdfname)) { + if (password == null) { + throw new IOException("PEM is encrypted, but no password was specified"); + } + + TypesReader optionsReader = new TypesReader(kdfoptions); + byte[] salt = optionsReader.readByteString(); + int rounds = optionsReader.readUINT32(); + byte[] passwordBytes; + try { + passwordBytes = password.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + passwordBytes = password.getBytes(); + } + dataBytes = decryptData(dataBytes, passwordBytes, salt, rounds, ciphername); + } else if (!"none".equals(ciphername) || !"none".equals(kdfname)) { + throw new IOException("encryption not supported"); + } + + TypesReader trEnc = new TypesReader(dataBytes); + + int checkInt1 = trEnc.readUINT32(); + int checkInt2 = trEnc.readUINT32(); + + if (checkInt1 != checkInt2) { + throw new IOException("Decryption failed when trying to read private keys"); + } + + String keyType = trEnc.readString(); + + KeyPair keyPair; + if (Ed25519Verify.ED25519_ID.equals(keyType)) { + byte[] publicBytes = trEnc.readByteString(); + byte[] privateBytes = trEnc.readByteString(); + PrivateKey privKey = new Ed25519PrivateKey( + Arrays.copyOfRange(privateBytes, 0, 32)); + PublicKey pubKey = new Ed25519PublicKey(publicBytes); + keyPair = new KeyPair(pubKey, privKey); + } else if (keyType.startsWith("ecdsa-sha2-")) { + String curveName = trEnc.readString(); + + byte[] groupBytes = trEnc.readByteString(); + BigInteger privateKey = trEnc.readMPINT(); + + final ECDSASHA2Verify verifier; + if (curveName.equals(ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().getCurveName())) { + verifier = ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get(); + } else if (curveName.equals(ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().getCurveName())) { + verifier = ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get(); + } else if (curveName.equals(ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().getCurveName())) { + verifier = ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get(); + } else { + throw new IOException("Invalid ECDSA group"); + } + + ECParameterSpec spec = verifier.getParameterSpec(); + ECPoint group = verifier.decodeECPoint(groupBytes); + + ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(group, spec); + ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(privateKey, spec); + keyPair = generateKeyPair("EC", privateKeySpec, publicKeySpec); + } else if (RSASHA1Verify.get().getKeyFormat().equals(keyType)) { + BigInteger n = trEnc.readMPINT(); + BigInteger e = trEnc.readMPINT(); + BigInteger d = trEnc.readMPINT(); + + BigInteger crtCoefficient = trEnc.readMPINT(); + BigInteger p = trEnc.readMPINT(); + + RSAPrivateKeySpec privateKeySpec; + if (null == p || null == crtCoefficient) { + privateKeySpec = new RSAPrivateKeySpec(n, d); + } else { + BigInteger q = crtCoefficient.modInverse(p); + BigInteger pE = d.mod(p.subtract(BigInteger.ONE)); + BigInteger qE = d.mod(q.subtract(BigInteger.ONE)); + privateKeySpec = new RSAPrivateCrtKeySpec(n, e, d, p, q, pE, qE, crtCoefficient); + + } + + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); + + keyPair = generateKeyPair("RSA", privateKeySpec, publicKeySpec); + } else if (DSASHA1Verify.get().getKeyFormat().equals(keyType)) { + BigInteger p = trEnc.readMPINT(); + BigInteger q = trEnc.readMPINT(); + BigInteger g = trEnc.readMPINT(); + BigInteger y = trEnc.readMPINT(); + BigInteger x = trEnc.readMPINT(); + + DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); + DSAPublicKeySpec publicKeySpec = new DSAPublicKeySpec(y, p, q, g); + + keyPair = generateKeyPair("DSA", privateKeySpec, publicKeySpec); + } else { + throw new IOException("Unknown key type " + keyType); + } + + byte[] comment = trEnc.readByteString(); + + // Make sure the padding is correct first. + int remaining = tr.remain(); + for (int i = 1; i <= remaining; i++) { + if (i != tr.readByte()) { + throw new IOException("Bad padding value on decrypted private keys"); + } + } + + return keyPair; + } + + throw new IOException("PEM problem: it is of unknown type"); + } + + /** + * Generate a {@code KeyPair} given an {@code algorithm} and {@code KeySpec}. + */ + private static KeyPair generateKeyPair(String algorithm, KeySpec privSpec, KeySpec pubSpec) + throws IOException { + try { + final KeyFactory kf = KeyFactory.getInstance(algorithm); + final PublicKey pubKey = kf.generatePublic(pubSpec); + final PrivateKey privKey = kf.generatePrivate(privSpec); + return new KeyPair(pubKey, privKey); + } catch (NoSuchAlgorithmException ex) { + throw new IOException(ex); + } catch (InvalidKeySpecException ex) { + throw new IOException("invalid keyspec", ex); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java new file mode 100644 index 0000000000..d7bb562920 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java @@ -0,0 +1,17 @@ + +package com.trilead.ssh2.crypto; + +/** + * Parsed PEM structure. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PEMStructure.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class PEMStructure +{ + public int pemType; + String dekInfo[]; + String procType[]; + public byte[] data; +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java new file mode 100644 index 0000000000..dca2e00272 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java @@ -0,0 +1,235 @@ +package com.trilead.ssh2.crypto; + +import java.io.IOException; + +import java.math.BigInteger; + +/** + * SimpleDERReader. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SimpleDERReader.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class SimpleDERReader +{ + private static final int CONSTRUCTED = 0x20; + + byte[] buffer; + int pos; + int count; + + public SimpleDERReader(byte[] b) + { + resetInput(b); + } + + public SimpleDERReader(byte[] b, int off, int len) + { + resetInput(b, off, len); + } + + public void resetInput(byte[] b) + { + resetInput(b, 0, b.length); + } + + public void resetInput(byte[] b, int off, int len) + { + buffer = b; + pos = off; + count = len; + } + + private byte readByte() throws IOException + { + if (count <= 0) + throw new IOException("DER byte array: out of data"); + count--; + return buffer[pos++]; + } + + private byte[] readBytes(int len) throws IOException + { + if (len > count) + throw new IOException("DER byte array: out of data"); + + byte[] b = new byte[len]; + + System.arraycopy(buffer, pos, b, 0, len); + + pos += len; + count -= len; + + return b; + } + + public int available() + { + return count; + } + + /* visible for testing */ + int readLength() throws IOException + { + int len = readByte() & 0xff; + + if ((len & 0x80) == 0) + return len; + + int remain = len & 0x7F; + + if (remain == 0) + return -1; + else if (remain > 4) + return -1; + + len = 0; + + while (remain > 0) + { + len = len << 8; + len = len | (readByte() & 0xff); + remain--; + } + + if (len < 0) + return -1; + + return len; + } + + public int ignoreNextObject() throws IOException + { + int type = readByte() & 0xff; + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + readBytes(len); + + return type; + } + + public BigInteger readInt() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x02) + throw new IOException("Expected DER Integer, but found type " + type); + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + BigInteger bi = new BigInteger(1, b); + + return bi; + } + + public int readConstructedType() throws IOException { + int type = readByte() & 0xff; + + if ((type & CONSTRUCTED) != CONSTRUCTED) + throw new IOException("Expected constructed type, but was " + type); + + return type & 0x1f; + } + + public SimpleDERReader readConstructed() throws IOException + { + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + SimpleDERReader cr = new SimpleDERReader(buffer, pos, len); + + pos += len; + count -= len; + + return cr; + } + + public byte[] readSequenceAsByteArray() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x30) + throw new IOException("Expected DER Sequence, but found type " + type); + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + return b; + } + + public String readOid() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x06) + throw new IOException("Expected DER OID, but found type " + type); + + int len = readLength(); + + if ((len < 1) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + long value = 0; + + StringBuilder sb = new StringBuilder(64); + switch (b[0] / 40) { + case 0: + sb.append('0'); + break; + case 1: + sb.append('1'); + b[0] -= 40; + break; + default: + sb.append('2'); + b[0] -= 80; + break; + } + + for (int i = 0; i < len; i++) { + value = (value << 7) + (b[i] & 0x7F); + if ((b[i] & 0x80) == 0) { + sb.append('.'); + sb.append(value); + value = 0; + } + } + + return sb.toString(); + } + + public byte[] readOctetString() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x04 && type != 0x03) + throw new IOException("Expected DER Octetstring, but found type " + type); + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + return b; + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java new file mode 100644 index 0000000000..fef4fafd32 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java @@ -0,0 +1,66 @@ + +package com.trilead.ssh2.crypto.cipher; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * AES modes for SSH using the JCE. + */ +public abstract class AES implements BlockCipher +{ + private final int AES_BLOCK_SIZE = 16; + + protected Cipher cipher; + + @Override + public void init(boolean forEncryption, byte[] key, byte[] iv) { + try { + cipher.init(forEncryption ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, + new SecretKeySpec(key, "AES"), + new IvParameterSpec(iv)); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException("Cannot initialize " + cipher.getAlgorithm(), e); + } + } + + @Override + public int getBlockSize() { + return AES_BLOCK_SIZE; + } + + @Override + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) { + try { + cipher.update(src, srcoff, AES_BLOCK_SIZE, dst, dstoff); + } catch (ShortBufferException e) { + throw new AssertionError(e); + } + } + + public static class CBC extends AES { + public CBC() throws IllegalArgumentException { + try { + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Cannot initialize AES/CBC/NoPadding", e); + } + } + } + + public static class CTR extends AES { + public CTR() throws IllegalArgumentException { + try { + cipher = Cipher.getInstance("AES/CTR/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Cannot initialize AES/CBC/NoPadding", e); + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java new file mode 100644 index 0000000000..7c1abd4179 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java @@ -0,0 +1,16 @@ +package com.trilead.ssh2.crypto.cipher; + +/** + * BlockCipher. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: BlockCipher.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public interface BlockCipher +{ + void init(boolean forEncryption, byte[] key, byte[] iv) throws IllegalArgumentException; + + int getBlockSize(); + + void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java new file mode 100644 index 0000000000..be734aaf35 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java @@ -0,0 +1,103 @@ + +package com.trilead.ssh2.crypto.cipher; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; + +/** + * BlockCipherFactory. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: BlockCipherFactory.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class BlockCipherFactory +{ + private static class CipherEntry + { + final String type; + final int blocksize; + final int keysize; + final String cipherClass; + + CipherEntry(String type, int blockSize, int keySize, String cipherClass) + { + this.type = type; + this.blocksize = blockSize; + this.keysize = keySize; + this.cipherClass = cipherClass; + } + } + + private static final ArrayList ciphers = new ArrayList<>(); + + static + { + /* Higher Priority First */ + + ciphers.add(new CipherEntry("aes256-ctr", 16, 32, "com.trilead.ssh2.crypto.cipher.AES$CTR")); + ciphers.add(new CipherEntry("aes128-ctr", 16, 16, "com.trilead.ssh2.crypto.cipher.AES$CTR")); + ciphers.add(new CipherEntry("blowfish-ctr", 8, 16, "com.trilead.ssh2.crypto.cipher.BlowFish$CTR")); + + ciphers.add(new CipherEntry("aes256-cbc", 16, 32, "com.trilead.ssh2.crypto.cipher.AES$CBC")); + ciphers.add(new CipherEntry("aes128-cbc", 16, 16, "com.trilead.ssh2.crypto.cipher.AES$CBC")); + ciphers.add(new CipherEntry("blowfish-cbc", 8, 16, "com.trilead.ssh2.crypto.cipher.BlowFish$CBC")); + + ciphers.add(new CipherEntry("3des-ctr", 8, 24, "com.trilead.ssh2.crypto.cipher.DESede$CTR")); + ciphers.add(new CipherEntry("3des-cbc", 8, 24, "com.trilead.ssh2.crypto.cipher.DESede$CBC")); + } + + public static String[] getDefaultCipherList() + { + String list[] = new String[ciphers.size()]; + for (int i = 0; i < ciphers.size(); i++) + { + CipherEntry ce = ciphers.get(i); + list[i] = ce.type; + } + return list; + } + + public static void checkCipherList(String[] cipherCandidates) + { + for (String cipherCandidate : cipherCandidates) + getEntry(cipherCandidate); + } + + public static BlockCipher createCipher(String type, boolean encrypt, byte[] key, byte[] iv) + { + try + { + CipherEntry ce = getEntry(type); + Class cc = Class.forName(ce.cipherClass); + Constructor constructor = cc.getConstructor(); + BlockCipher bc = constructor.newInstance(); + bc.init(encrypt, key, iv); + return bc; + } + catch (Exception e) + { + throw new IllegalArgumentException("Cannot instantiate " + type, e); + } + } + + private static CipherEntry getEntry(String type) + { + for (CipherEntry ce : ciphers) { + if (ce.type.equals(type)) + return ce; + } + throw new IllegalArgumentException("Unknown algorithm " + type); + } + + public static int getBlockSize(String type) + { + CipherEntry ce = getEntry(type); + return ce.blocksize; + } + + public static int getKeySize(String type) + { + CipherEntry ce = getEntry(type); + return ce.keysize; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java new file mode 100644 index 0000000000..1727fc068b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java @@ -0,0 +1,435 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + * This file was shamelessly taken from the Bouncy Castle Crypto package. + * Their licence file states the following: + * + * Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + * (http://www.bouncycastle.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A class that provides Blowfish key encryption operations, such as encoding + * data and generating keys. All the algorithms herein are from Applied + * Cryptography and implement a simplified cryptography interface. + * + * @author See comments in the source file + * @version $Id: BlowFish.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class BlowFish implements BlockCipher +{ + + private final static int[] KP = { 0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344, 0xA4093822, 0x299F31D0, + 0x082EFA98, 0xEC4E6C89, 0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C, 0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, + 0xB5470917, 0x9216D5D9, 0x8979FB1B }, + + KS0 = { 0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7, 0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99, 0x24A19947, + 0xB3916CF7, 0x0801F2E2, 0x858EFC16, 0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E, 0x0D95748F, 0x728EB658, + 0x718BCD58, 0x82154AEE, 0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013, 0xC5D1B023, 0x286085F0, 0xCA417918, + 0xB8DB38EF, 0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E, 0xD71577C1, 0xBD314B27, 0x78AF2FDA, 0x55605C60, + 0xE65525F3, 0xAA55AB94, 0x57489862, 0x63E81440, 0x55CA396A, 0x2AAB10B6, 0xB4CC5C34, 0x1141E8CE, 0xA15486AF, + 0x7C72E993, 0xB3EE1411, 0x636FBC2A, 0x2BA9C55D, 0x741831F6, 0xCE5C3E16, 0x9B87931E, 0xAFD6BA33, 0x6C24CF5C, + 0x7A325381, 0x28958677, 0x3B8F4898, 0x6B4BB9AF, 0xC4BFE81B, 0x66282193, 0x61D809CC, 0xFB21A991, 0x487CAC60, + 0x5DEC8032, 0xEF845D5D, 0xE98575B1, 0xDC262302, 0xEB651B88, 0x23893E81, 0xD396ACC5, 0x0F6D6FF3, 0x83F44239, + 0x2E0B4482, 0xA4842004, 0x69C8F04A, 0x9E1F9B5E, 0x21C66842, 0xF6E96C9A, 0x670C9C61, 0xABD388F0, 0x6A51A0D2, + 0xD8542F68, 0x960FA728, 0xAB5133A3, 0x6EEF0B6C, 0x137A3BE4, 0xBA3BF050, 0x7EFB2A98, 0xA1F1651D, 0x39AF0176, + 0x66CA593E, 0x82430E88, 0x8CEE8619, 0x456F9FB4, 0x7D84A5C3, 0x3B8B5EBE, 0xE06F75D8, 0x85C12073, 0x401A449F, + 0x56C16AA6, 0x4ED3AA62, 0x363F7706, 0x1BFEDF72, 0x429B023D, 0x37D0D724, 0xD00A1248, 0xDB0FEAD3, 0x49F1C09B, + 0x075372C9, 0x80991B7B, 0x25D479D8, 0xF6E8DEF7, 0xE3FE501A, 0xB6794C3B, 0x976CE0BD, 0x04C006BA, 0xC1A94FB6, + 0x409F60C4, 0x5E5C9EC2, 0x196A2463, 0x68FB6FAF, 0x3E6C53B5, 0x1339B2EB, 0x3B52EC6F, 0x6DFC511F, 0x9B30952C, + 0xCC814544, 0xAF5EBD09, 0xBEE3D004, 0xDE334AFD, 0x660F2807, 0x192E4BB3, 0xC0CBA857, 0x45C8740F, 0xD20B5F39, + 0xB9D3FBDB, 0x5579C0BD, 0x1A60320A, 0xD6A100C6, 0x402C7279, 0x679F25FE, 0xFB1FA3CC, 0x8EA5E9F8, 0xDB3222F8, + 0x3C7516DF, 0xFD616B15, 0x2F501EC8, 0xAD0552AB, 0x323DB5FA, 0xFD238760, 0x53317B48, 0x3E00DF82, 0x9E5C57BB, + 0xCA6F8CA0, 0x1A87562E, 0xDF1769DB, 0xD542A8F6, 0x287EFFC3, 0xAC6732C6, 0x8C4F5573, 0x695B27B0, 0xBBCA58C8, + 0xE1FFA35D, 0xB8F011A0, 0x10FA3D98, 0xFD2183B8, 0x4AFCB56C, 0x2DD1D35B, 0x9A53E479, 0xB6F84565, 0xD28E49BC, + 0x4BFB9790, 0xE1DDF2DA, 0xA4CB7E33, 0x62FB1341, 0xCEE4C6E8, 0xEF20CADA, 0x36774C01, 0xD07E9EFE, 0x2BF11FB4, + 0x95DBDA4D, 0xAE909198, 0xEAAD8E71, 0x6B93D5A0, 0xD08ED1D0, 0xAFC725E0, 0x8E3C5B2F, 0x8E7594B7, 0x8FF6E2FB, + 0xF2122B64, 0x8888B812, 0x900DF01C, 0x4FAD5EA0, 0x688FC31C, 0xD1CFF191, 0xB3A8C1AD, 0x2F2F2218, 0xBE0E1777, + 0xEA752DFE, 0x8B021FA1, 0xE5A0CC0F, 0xB56F74E8, 0x18ACF3D6, 0xCE89E299, 0xB4A84FE0, 0xFD13E0B7, 0x7CC43B81, + 0xD2ADA8D9, 0x165FA266, 0x80957705, 0x93CC7314, 0x211A1477, 0xE6AD2065, 0x77B5FA86, 0xC75442F5, 0xFB9D35CF, + 0xEBCDAF0C, 0x7B3E89A0, 0xD6411BD3, 0xAE1E7E49, 0x00250E2D, 0x2071B35E, 0x226800BB, 0x57B8E0AF, 0x2464369B, + 0xF009B91E, 0x5563911D, 0x59DFA6AA, 0x78C14389, 0xD95A537F, 0x207D5BA2, 0x02E5B9C5, 0x83260376, 0x6295CFA9, + 0x11C81968, 0x4E734A41, 0xB3472DCA, 0x7B14A94A, 0x1B510052, 0x9A532915, 0xD60F573F, 0xBC9BC6E4, 0x2B60A476, + 0x81E67400, 0x08BA6FB5, 0x571BE91F, 0xF296EC6B, 0x2A0DD915, 0xB6636521, 0xE7B9F9B6, 0xFF34052E, 0xC5855664, + 0x53B02D5D, 0xA99F8FA1, 0x08BA4799, 0x6E85076A }, + + KS1 = { 0x4B7A70E9, 0xB5B32944, 0xDB75092E, 0xC4192623, 0xAD6EA6B0, 0x49A7DF7D, 0x9CEE60B8, 0x8FEDB266, 0xECAA8C71, + 0x699A17FF, 0x5664526C, 0xC2B19EE1, 0x193602A5, 0x75094C29, 0xA0591340, 0xE4183A3E, 0x3F54989A, 0x5B429D65, + 0x6B8FE4D6, 0x99F73FD6, 0xA1D29C07, 0xEFE830F5, 0x4D2D38E6, 0xF0255DC1, 0x4CDD2086, 0x8470EB26, 0x6382E9C6, + 0x021ECC5E, 0x09686B3F, 0x3EBAEFC9, 0x3C971814, 0x6B6A70A1, 0x687F3584, 0x52A0E286, 0xB79C5305, 0xAA500737, + 0x3E07841C, 0x7FDEAE5C, 0x8E7D44EC, 0x5716F2B8, 0xB03ADA37, 0xF0500C0D, 0xF01C1F04, 0x0200B3FF, 0xAE0CF51A, + 0x3CB574B2, 0x25837A58, 0xDC0921BD, 0xD19113F9, 0x7CA92FF6, 0x94324773, 0x22F54701, 0x3AE5E581, 0x37C2DADC, + 0xC8B57634, 0x9AF3DDA7, 0xA9446146, 0x0FD0030E, 0xECC8C73E, 0xA4751E41, 0xE238CD99, 0x3BEA0E2F, 0x3280BBA1, + 0x183EB331, 0x4E548B38, 0x4F6DB908, 0x6F420D03, 0xF60A04BF, 0x2CB81290, 0x24977C79, 0x5679B072, 0xBCAF89AF, + 0xDE9A771F, 0xD9930810, 0xB38BAE12, 0xDCCF3F2E, 0x5512721F, 0x2E6B7124, 0x501ADDE6, 0x9F84CD87, 0x7A584718, + 0x7408DA17, 0xBC9F9ABC, 0xE94B7D8C, 0xEC7AEC3A, 0xDB851DFA, 0x63094366, 0xC464C3D2, 0xEF1C1847, 0x3215D908, + 0xDD433B37, 0x24C2BA16, 0x12A14D43, 0x2A65C451, 0x50940002, 0x133AE4DD, 0x71DFF89E, 0x10314E55, 0x81AC77D6, + 0x5F11199B, 0x043556F1, 0xD7A3C76B, 0x3C11183B, 0x5924A509, 0xF28FE6ED, 0x97F1FBFA, 0x9EBABF2C, 0x1E153C6E, + 0x86E34570, 0xEAE96FB1, 0x860E5E0A, 0x5A3E2AB3, 0x771FE71C, 0x4E3D06FA, 0x2965DCB9, 0x99E71D0F, 0x803E89D6, + 0x5266C825, 0x2E4CC978, 0x9C10B36A, 0xC6150EBA, 0x94E2EA78, 0xA5FC3C53, 0x1E0A2DF4, 0xF2F74EA7, 0x361D2B3D, + 0x1939260F, 0x19C27960, 0x5223A708, 0xF71312B6, 0xEBADFE6E, 0xEAC31F66, 0xE3BC4595, 0xA67BC883, 0xB17F37D1, + 0x018CFF28, 0xC332DDEF, 0xBE6C5AA5, 0x65582185, 0x68AB9802, 0xEECEA50F, 0xDB2F953B, 0x2AEF7DAD, 0x5B6E2F84, + 0x1521B628, 0x29076170, 0xECDD4775, 0x619F1510, 0x13CCA830, 0xEB61BD96, 0x0334FE1E, 0xAA0363CF, 0xB5735C90, + 0x4C70A239, 0xD59E9E0B, 0xCBAADE14, 0xEECC86BC, 0x60622CA7, 0x9CAB5CAB, 0xB2F3846E, 0x648B1EAF, 0x19BDF0CA, + 0xA02369B9, 0x655ABB50, 0x40685A32, 0x3C2AB4B3, 0x319EE9D5, 0xC021B8F7, 0x9B540B19, 0x875FA099, 0x95F7997E, + 0x623D7DA8, 0xF837889A, 0x97E32D77, 0x11ED935F, 0x16681281, 0x0E358829, 0xC7E61FD6, 0x96DEDFA1, 0x7858BA99, + 0x57F584A5, 0x1B227263, 0x9B83C3FF, 0x1AC24696, 0xCDB30AEB, 0x532E3054, 0x8FD948E4, 0x6DBC3128, 0x58EBF2EF, + 0x34C6FFEA, 0xFE28ED61, 0xEE7C3C73, 0x5D4A14D9, 0xE864B7E3, 0x42105D14, 0x203E13E0, 0x45EEE2B6, 0xA3AAABEA, + 0xDB6C4F15, 0xFACB4FD0, 0xC742F442, 0xEF6ABBB5, 0x654F3B1D, 0x41CD2105, 0xD81E799E, 0x86854DC7, 0xE44B476A, + 0x3D816250, 0xCF62A1F2, 0x5B8D2646, 0xFC8883A0, 0xC1C7B6A3, 0x7F1524C3, 0x69CB7492, 0x47848A0B, 0x5692B285, + 0x095BBF00, 0xAD19489D, 0x1462B174, 0x23820E00, 0x58428D2A, 0x0C55F5EA, 0x1DADF43E, 0x233F7061, 0x3372F092, + 0x8D937E41, 0xD65FECF1, 0x6C223BDB, 0x7CDE3759, 0xCBEE7460, 0x4085F2A7, 0xCE77326E, 0xA6078084, 0x19F8509E, + 0xE8EFD855, 0x61D99735, 0xA969A7AA, 0xC50C06C2, 0x5A04ABFC, 0x800BCADC, 0x9E447A2E, 0xC3453484, 0xFDD56705, + 0x0E1E9EC9, 0xDB73DBD3, 0x105588CD, 0x675FDA79, 0xE3674340, 0xC5C43465, 0x713E38D8, 0x3D28F89E, 0xF16DFF20, + 0x153E21E7, 0x8FB03D4A, 0xE6E39F2B, 0xDB83ADF7 }, + + KS2 = { 0xE93D5A68, 0x948140F7, 0xF64C261C, 0x94692934, 0x411520F7, 0x7602D4F7, 0xBCF46B2E, 0xD4A20068, 0xD4082471, + 0x3320F46A, 0x43B7D4B7, 0x500061AF, 0x1E39F62E, 0x97244546, 0x14214F74, 0xBF8B8840, 0x4D95FC1D, 0x96B591AF, + 0x70F4DDD3, 0x66A02F45, 0xBFBC09EC, 0x03BD9785, 0x7FAC6DD0, 0x31CB8504, 0x96EB27B3, 0x55FD3941, 0xDA2547E6, + 0xABCA0A9A, 0x28507825, 0x530429F4, 0x0A2C86DA, 0xE9B66DFB, 0x68DC1462, 0xD7486900, 0x680EC0A4, 0x27A18DEE, + 0x4F3FFEA2, 0xE887AD8C, 0xB58CE006, 0x7AF4D6B6, 0xAACE1E7C, 0xD3375FEC, 0xCE78A399, 0x406B2A42, 0x20FE9E35, + 0xD9F385B9, 0xEE39D7AB, 0x3B124E8B, 0x1DC9FAF7, 0x4B6D1856, 0x26A36631, 0xEAE397B2, 0x3A6EFA74, 0xDD5B4332, + 0x6841E7F7, 0xCA7820FB, 0xFB0AF54E, 0xD8FEB397, 0x454056AC, 0xBA489527, 0x55533A3A, 0x20838D87, 0xFE6BA9B7, + 0xD096954B, 0x55A867BC, 0xA1159A58, 0xCCA92963, 0x99E1DB33, 0xA62A4A56, 0x3F3125F9, 0x5EF47E1C, 0x9029317C, + 0xFDF8E802, 0x04272F70, 0x80BB155C, 0x05282CE3, 0x95C11548, 0xE4C66D22, 0x48C1133F, 0xC70F86DC, 0x07F9C9EE, + 0x41041F0F, 0x404779A4, 0x5D886E17, 0x325F51EB, 0xD59BC0D1, 0xF2BCC18F, 0x41113564, 0x257B7834, 0x602A9C60, + 0xDFF8E8A3, 0x1F636C1B, 0x0E12B4C2, 0x02E1329E, 0xAF664FD1, 0xCAD18115, 0x6B2395E0, 0x333E92E1, 0x3B240B62, + 0xEEBEB922, 0x85B2A20E, 0xE6BA0D99, 0xDE720C8C, 0x2DA2F728, 0xD0127845, 0x95B794FD, 0x647D0862, 0xE7CCF5F0, + 0x5449A36F, 0x877D48FA, 0xC39DFD27, 0xF33E8D1E, 0x0A476341, 0x992EFF74, 0x3A6F6EAB, 0xF4F8FD37, 0xA812DC60, + 0xA1EBDDF8, 0x991BE14C, 0xDB6E6B0D, 0xC67B5510, 0x6D672C37, 0x2765D43B, 0xDCD0E804, 0xF1290DC7, 0xCC00FFA3, + 0xB5390F92, 0x690FED0B, 0x667B9FFB, 0xCEDB7D9C, 0xA091CF0B, 0xD9155EA3, 0xBB132F88, 0x515BAD24, 0x7B9479BF, + 0x763BD6EB, 0x37392EB3, 0xCC115979, 0x8026E297, 0xF42E312D, 0x6842ADA7, 0xC66A2B3B, 0x12754CCC, 0x782EF11C, + 0x6A124237, 0xB79251E7, 0x06A1BBE6, 0x4BFB6350, 0x1A6B1018, 0x11CAEDFA, 0x3D25BDD8, 0xE2E1C3C9, 0x44421659, + 0x0A121386, 0xD90CEC6E, 0xD5ABEA2A, 0x64AF674E, 0xDA86A85F, 0xBEBFE988, 0x64E4C3FE, 0x9DBC8057, 0xF0F7C086, + 0x60787BF8, 0x6003604D, 0xD1FD8346, 0xF6381FB0, 0x7745AE04, 0xD736FCCC, 0x83426B33, 0xF01EAB71, 0xB0804187, + 0x3C005E5F, 0x77A057BE, 0xBDE8AE24, 0x55464299, 0xBF582E61, 0x4E58F48F, 0xF2DDFDA2, 0xF474EF38, 0x8789BDC2, + 0x5366F9C3, 0xC8B38E74, 0xB475F255, 0x46FCD9B9, 0x7AEB2661, 0x8B1DDF84, 0x846A0E79, 0x915F95E2, 0x466E598E, + 0x20B45770, 0x8CD55591, 0xC902DE4C, 0xB90BACE1, 0xBB8205D0, 0x11A86248, 0x7574A99E, 0xB77F19B6, 0xE0A9DC09, + 0x662D09A1, 0xC4324633, 0xE85A1F02, 0x09F0BE8C, 0x4A99A025, 0x1D6EFE10, 0x1AB93D1D, 0x0BA5A4DF, 0xA186F20F, + 0x2868F169, 0xDCB7DA83, 0x573906FE, 0xA1E2CE9B, 0x4FCD7F52, 0x50115E01, 0xA70683FA, 0xA002B5C4, 0x0DE6D027, + 0x9AF88C27, 0x773F8641, 0xC3604C06, 0x61A806B5, 0xF0177A28, 0xC0F586E0, 0x006058AA, 0x30DC7D62, 0x11E69ED7, + 0x2338EA63, 0x53C2DD94, 0xC2C21634, 0xBBCBEE56, 0x90BCB6DE, 0xEBFC7DA1, 0xCE591D76, 0x6F05E409, 0x4B7C0188, + 0x39720A3D, 0x7C927C24, 0x86E3725F, 0x724D9DB9, 0x1AC15BB4, 0xD39EB8FC, 0xED545578, 0x08FCA5B5, 0xD83D7CD3, + 0x4DAD0FC4, 0x1E50EF5E, 0xB161E6F8, 0xA28514D9, 0x6C51133C, 0x6FD5C7E7, 0x56E14EC4, 0x362ABFCE, 0xDDC6C837, + 0xD79A3234, 0x92638212, 0x670EFA8E, 0x406000E0 }, + + KS3 = { 0x3A39CE37, 0xD3FAF5CF, 0xABC27737, 0x5AC52D1B, 0x5CB0679E, 0x4FA33742, 0xD3822740, 0x99BC9BBE, 0xD5118E9D, + 0xBF0F7315, 0xD62D1C7E, 0xC700C47B, 0xB78C1B6B, 0x21A19045, 0xB26EB1BE, 0x6A366EB4, 0x5748AB2F, 0xBC946E79, + 0xC6A376D2, 0x6549C2C8, 0x530FF8EE, 0x468DDE7D, 0xD5730A1D, 0x4CD04DC6, 0x2939BBDB, 0xA9BA4650, 0xAC9526E8, + 0xBE5EE304, 0xA1FAD5F0, 0x6A2D519A, 0x63EF8CE2, 0x9A86EE22, 0xC089C2B8, 0x43242EF6, 0xA51E03AA, 0x9CF2D0A4, + 0x83C061BA, 0x9BE96A4D, 0x8FE51550, 0xBA645BD6, 0x2826A2F9, 0xA73A3AE1, 0x4BA99586, 0xEF5562E9, 0xC72FEFD3, + 0xF752F7DA, 0x3F046F69, 0x77FA0A59, 0x80E4A915, 0x87B08601, 0x9B09E6AD, 0x3B3EE593, 0xE990FD5A, 0x9E34D797, + 0x2CF0B7D9, 0x022B8B51, 0x96D5AC3A, 0x017DA67D, 0xD1CF3ED6, 0x7C7D2D28, 0x1F9F25CF, 0xADF2B89B, 0x5AD6B472, + 0x5A88F54C, 0xE029AC71, 0xE019A5E6, 0x47B0ACFD, 0xED93FA9B, 0xE8D3C48D, 0x283B57CC, 0xF8D56629, 0x79132E28, + 0x785F0191, 0xED756055, 0xF7960E44, 0xE3D35E8C, 0x15056DD4, 0x88F46DBA, 0x03A16125, 0x0564F0BD, 0xC3EB9E15, + 0x3C9057A2, 0x97271AEC, 0xA93A072A, 0x1B3F6D9B, 0x1E6321F5, 0xF59C66FB, 0x26DCF319, 0x7533D928, 0xB155FDF5, + 0x03563482, 0x8ABA3CBB, 0x28517711, 0xC20AD9F8, 0xABCC5167, 0xCCAD925F, 0x4DE81751, 0x3830DC8E, 0x379D5862, + 0x9320F991, 0xEA7A90C2, 0xFB3E7BCE, 0x5121CE64, 0x774FBE32, 0xA8B6E37E, 0xC3293D46, 0x48DE5369, 0x6413E680, + 0xA2AE0810, 0xDD6DB224, 0x69852DFD, 0x09072166, 0xB39A460A, 0x6445C0DD, 0x586CDECF, 0x1C20C8AE, 0x5BBEF7DD, + 0x1B588D40, 0xCCD2017F, 0x6BB4E3BB, 0xDDA26A7E, 0x3A59FF45, 0x3E350A44, 0xBCB4CDD5, 0x72EACEA8, 0xFA6484BB, + 0x8D6612AE, 0xBF3C6F47, 0xD29BE463, 0x542F5D9E, 0xAEC2771B, 0xF64E6370, 0x740E0D8D, 0xE75B1357, 0xF8721671, + 0xAF537D5D, 0x4040CB08, 0x4EB4E2CC, 0x34D2466A, 0x0115AF84, 0xE1B00428, 0x95983A1D, 0x06B89FB4, 0xCE6EA048, + 0x6F3F3B82, 0x3520AB82, 0x011A1D4B, 0x277227F8, 0x611560B1, 0xE7933FDC, 0xBB3A792B, 0x344525BD, 0xA08839E1, + 0x51CE794B, 0x2F32C9B7, 0xA01FBAC9, 0xE01CC87E, 0xBCC7D1F6, 0xCF0111C3, 0xA1E8AAC7, 0x1A908749, 0xD44FBD9A, + 0xD0DADECB, 0xD50ADA38, 0x0339C32A, 0xC6913667, 0x8DF9317C, 0xE0B12B4F, 0xF79E59B7, 0x43F5BB3A, 0xF2D519FF, + 0x27D9459C, 0xBF97222C, 0x15E6FC2A, 0x0F91FC71, 0x9B941525, 0xFAE59361, 0xCEB69CEB, 0xC2A86459, 0x12BAA8D1, + 0xB6C1075E, 0xE3056A0C, 0x10D25065, 0xCB03A442, 0xE0EC6E0E, 0x1698DB3B, 0x4C98A0BE, 0x3278E964, 0x9F1F9532, + 0xE0D392DF, 0xD3A0342B, 0x8971F21E, 0x1B0A7441, 0x4BA3348C, 0xC5BE7120, 0xC37632D8, 0xDF359F8D, 0x9B992F2E, + 0xE60B6F47, 0x0FE3F11D, 0xE54CDA54, 0x1EDAD891, 0xCE6279CF, 0xCD3E7E6F, 0x1618B166, 0xFD2C1D05, 0x848FD2C5, + 0xF6FB2299, 0xF523F357, 0xA6327623, 0x93A83531, 0x56CCCD02, 0xACF08162, 0x5A75EBB5, 0x6E163697, 0x88D273CC, + 0xDE966292, 0x81B949D0, 0x4C50901B, 0x71C65614, 0xE6C6C7BD, 0x327A140A, 0x45E1D006, 0xC3F27B9A, 0xC9AA53FD, + 0x62A80F00, 0xBB25BFE2, 0x35BDD2F6, 0x71126905, 0xB2040222, 0xB6CBCF7C, 0xCD769C2B, 0x53113EC0, 0x1640E3D3, + 0x38ABBD60, 0x2547ADF0, 0xBA38209C, 0xF746CE76, 0x77AFA1C5, 0x20756060, 0x85CBFE4E, 0x8AE88DD8, 0x7AAAF9B0, + 0x4CF9AA7E, 0x1948C25C, 0x02FB8A8C, 0x01C36AE4, 0xD6EBE1F9, 0x90D4F869, 0xA65CDEA0, 0x3F09252D, 0xC208E69F, + 0xB74E6132, 0xCE77E25B, 0x578FDFE3, 0x3AC372E6 }; + + // ==================================== + // Useful constants + // ==================================== + + private static final int ROUNDS = 16; + private static final int BLOCK_SIZE = 8; // bytes = 64 bits + private static final int SBOX_SK = 256; + private static final int P_SZ = ROUNDS + 2; + + private final int[] S0, S1, S2, S3; // the s-boxes + private final int[] P; // the p-array + + private boolean doEncrypt = false; + + private byte[] workingKey = null; + + public BlowFish() + { + S0 = new int[SBOX_SK]; + S1 = new int[SBOX_SK]; + S2 = new int[SBOX_SK]; + S3 = new int[SBOX_SK]; + P = new int[P_SZ]; + } + + /** + * initialise a Blowfish cipher. + * + * @param encrypting + * whether or not we are for encryption. + * @param key + * the key required to set up the cipher. + * @param iv + * initial vector; not used for stream ciphers + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + @Override + public void init(boolean encrypting, byte[] key, byte[] iv) + { + this.doEncrypt = encrypting; + this.workingKey = key; + setKey(this.workingKey); + } + + public String getAlgorithmName() + { + return "Blowfish"; + } + + @Override + public final void transformBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (workingKey == null) + { + throw new IllegalStateException("Blowfish not initialised"); + } + + if (doEncrypt) + { + encryptBlock(in, inOff, out, outOff); + } + else + { + decryptBlock(in, inOff, out, outOff); + } + } + + @Override + public int getBlockSize() + { + return BLOCK_SIZE; + } + + // ================================== + // Private Implementation + // ================================== + + private int F(int x) + { + return (((S0[(x >>> 24)] + S1[(x >>> 16) & 0xff]) ^ S2[(x >>> 8) & 0xff]) + S3[x & 0xff]); + } + + /** + * apply the encryption cycle to each value pair in the table. + */ + private void processTable(int xl, int xr, int[] table) + { + int size = table.length; + + for (int s = 0; s < size; s += 2) + { + xl ^= P[0]; + + for (int i = 1; i < ROUNDS; i += 2) + { + xr ^= F(xl) ^ P[i]; + xl ^= F(xr) ^ P[i + 1]; + } + + xr ^= P[ROUNDS + 1]; + + table[s] = xr; + table[s + 1] = xl; + + xr = xl; // end of cycle swap + xl = table[s]; + } + } + + private void setKey(byte[] key) + { + /* + * - comments are from _Applied Crypto_, Schneier, p338 please be + * careful comparing the two, AC numbers the arrays from 1, the enclosed + * code from 0. + * + * (1) Initialise the S-boxes and the P-array, with a fixed string This + * string contains the hexadecimal digits of pi (3.141...) + */ + System.arraycopy(KS0, 0, S0, 0, SBOX_SK); + System.arraycopy(KS1, 0, S1, 0, SBOX_SK); + System.arraycopy(KS2, 0, S2, 0, SBOX_SK); + System.arraycopy(KS3, 0, S3, 0, SBOX_SK); + + System.arraycopy(KP, 0, P, 0, P_SZ); + + /* + * (2) Now, XOR P[0] with the first 32 bits of the key, XOR P[1] with + * the second 32-bits of the key, and so on for all bits of the key (up + * to P[17]). Repeatedly cycle through the key bits until the entire + * P-array has been XOR-ed with the key bits + */ + int keyLength = key.length; + int keyIndex = 0; + + for (int i = 0; i < P_SZ; i++) + { + // get the 32 bits of the key, in 4 * 8 bit chunks + int data = 0x0000000; + for (int j = 0; j < 4; j++) + { + // create a 32 bit block + data = (data << 8) | (key[keyIndex++] & 0xff); + + // wrap when we get to the end of the key + if (keyIndex >= keyLength) + { + keyIndex = 0; + } + } + // XOR the newly created 32 bit chunk onto the P-array + P[i] ^= data; + } + + /* + * (3) Encrypt the all-zero string with the Blowfish algorithm, using + * the subkeys described in (1) and (2) + * + * (4) Replace P1 and P2 with the output of step (3) + * + * (5) Encrypt the output of step(3) using the Blowfish algorithm, with + * the modified subkeys. + * + * (6) Replace P3 and P4 with the output of step (5) + * + * (7) Continue the process, replacing all elements of the P-array and + * then all four S-boxes in order, with the output of the continuously + * changing Blowfish algorithm + */ + + processTable(0, 0, P); + processTable(P[P_SZ - 2], P[P_SZ - 1], S0); + processTable(S0[SBOX_SK - 2], S0[SBOX_SK - 1], S1); + processTable(S1[SBOX_SK - 2], S1[SBOX_SK - 1], S2); + processTable(S2[SBOX_SK - 2], S2[SBOX_SK - 1], S3); + } + + /** + * Encrypt the given input starting at the given offset and place the result + * in the provided buffer starting at the given offset. The input will be an + * exact multiple of our blocksize. + */ + private void encryptBlock(byte[] src, int srcIndex, byte[] dst, int dstIndex) + { + int xl = BytesTo32bits(src, srcIndex); + int xr = BytesTo32bits(src, srcIndex + 4); + + xl ^= P[0]; + + for (int i = 1; i < ROUNDS; i += 2) + { + xr ^= F(xl) ^ P[i]; + xl ^= F(xr) ^ P[i + 1]; + } + + xr ^= P[ROUNDS + 1]; + + Bits32ToBytes(xr, dst, dstIndex); + Bits32ToBytes(xl, dst, dstIndex + 4); + } + + /** + * Decrypt the given input starting at the given offset and place the result + * in the provided buffer starting at the given offset. The input will be an + * exact multiple of our blocksize. + */ + private void decryptBlock(byte[] src, int srcIndex, byte[] dst, int dstIndex) + { + int xl = BytesTo32bits(src, srcIndex); + int xr = BytesTo32bits(src, srcIndex + 4); + + xl ^= P[ROUNDS + 1]; + + for (int i = ROUNDS; i > 0; i -= 2) + { + xr ^= F(xl) ^ P[i]; + xl ^= F(xr) ^ P[i - 1]; + } + + xr ^= P[0]; + + Bits32ToBytes(xr, dst, dstIndex); + Bits32ToBytes(xl, dst, dstIndex + 4); + } + + private int BytesTo32bits(byte[] b, int i) + { + return ((b[i] & 0xff) << 24) | ((b[i + 1] & 0xff) << 16) | ((b[i + 2] & 0xff) << 8) | ((b[i + 3] & 0xff)); + } + + private void Bits32ToBytes(int in, byte[] b, int offset) + { + b[offset + 3] = (byte) in; + b[offset + 2] = (byte) (in >> 8); + b[offset + 1] = (byte) (in >> 16); + b[offset] = (byte) (in >> 24); + } + + private abstract static class Wrapper implements BlockCipher { + protected BlockCipher bc; + + @Override + public int getBlockSize() { + return bc.getBlockSize(); + } + + @Override + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) { + bc.transformBlock(src, srcoff, dst, dstoff); + } + } + + public static class CBC extends Wrapper { + @Override + public void init(boolean forEncryption, byte[] key, byte[] iv) throws IllegalArgumentException { + BlockCipher rawCipher = new BlowFish(); + rawCipher.init(forEncryption, key, iv); + bc = new CBCMode(rawCipher, iv, forEncryption); + } + } + + public static class CTR extends Wrapper { + @Override + public void init(boolean forEncryption, byte[] key, byte[] iv) throws IllegalArgumentException { + BlockCipher rawCipher = new BlowFish(); + rawCipher.init(true, key, iv); + bc = new CTRMode(rawCipher, iv, forEncryption); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java new file mode 100644 index 0000000000..078aacdc37 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java @@ -0,0 +1,78 @@ +package com.trilead.ssh2.crypto.cipher; + +/** + * CBCMode. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CBCMode.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CBCMode implements BlockCipher +{ + BlockCipher tc; + int blockSize; + boolean doEncrypt; + + byte[] cbc_vector; + byte[] tmp_vector; + + public void init(boolean forEncryption, byte[] key, byte[] iv) + { + } + + public CBCMode(BlockCipher tc, byte[] iv, boolean doEncrypt) + throws IllegalArgumentException + { + this.tc = tc; + this.blockSize = tc.getBlockSize(); + this.doEncrypt = doEncrypt; + + if (this.blockSize != iv.length) + throw new IllegalArgumentException("IV must be " + blockSize + + " bytes long! (currently " + iv.length + ")"); + + this.cbc_vector = new byte[blockSize]; + this.tmp_vector = new byte[blockSize]; + System.arraycopy(iv, 0, cbc_vector, 0, blockSize); + } + + public int getBlockSize() + { + return blockSize; + } + + private void encryptBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + for (int i = 0; i < blockSize; i++) + cbc_vector[i] ^= src[srcoff + i]; + + tc.transformBlock(cbc_vector, 0, dst, dstoff); + + System.arraycopy(dst, dstoff, cbc_vector, 0, blockSize); + } + + private void decryptBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + /* Assume the worst, src and dst are overlapping... */ + + System.arraycopy(src, srcoff, tmp_vector, 0, blockSize); + + tc.transformBlock(src, srcoff, dst, dstoff); + + for (int i = 0; i < blockSize; i++) + dst[dstoff + i] ^= cbc_vector[i]; + + /* ...that is why we need a tmp buffer. */ + + byte[] swap = cbc_vector; + cbc_vector = tmp_vector; + tmp_vector = swap; + } + + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + if (doEncrypt) + encryptBlock(src, srcoff, dst, dstoff); + else + decryptBlock(src, srcoff, dst, dstoff); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java new file mode 100644 index 0000000000..2c68f4abaa --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java @@ -0,0 +1,62 @@ + +package com.trilead.ssh2.crypto.cipher; + +/** + * This is CTR mode as described in draft-ietf-secsh-newmodes-XY.txt + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CTRMode.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CTRMode implements BlockCipher +{ + byte[] X; + byte[] Xenc; + + BlockCipher bc; + int blockSize; + boolean doEncrypt; + + int count = 0; + + public void init(boolean forEncryption, byte[] key, byte[] iv) + { + } + + public CTRMode(BlockCipher tc, byte[] iv, boolean doEnc) throws IllegalArgumentException + { + bc = tc; + blockSize = bc.getBlockSize(); + doEncrypt = doEnc; + + if (blockSize != iv.length) + throw new IllegalArgumentException("IV must be " + blockSize + " bytes long! (currently " + iv.length + ")"); + + X = new byte[blockSize]; + Xenc = new byte[blockSize]; + + System.arraycopy(iv, 0, X, 0, blockSize); + } + + public final int getBlockSize() + { + return blockSize; + } + + public final void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + bc.transformBlock(X, 0, Xenc, 0); + + for (int i = 0; i < blockSize; i++) + { + dst[dstoff + i] = (byte) (src[srcoff + i] ^ Xenc[i]); + } + + for (int i = (blockSize - 1); i >= 0; i--) + { + X[i]++; + if (X[i] != 0) + break; + + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java new file mode 100644 index 0000000000..851cc2843a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java @@ -0,0 +1,133 @@ + +package com.trilead.ssh2.crypto.cipher; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * CipherInputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CipherInputStream.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CipherInputStream +{ + private BlockCipher currentCipher; + private final BufferedInputStream bi; + private byte[] buffer; + private byte[] enc; + private int blockSize; + private int pos; + + public CipherInputStream(BlockCipher tc, InputStream bi) + { + if (bi instanceof BufferedInputStream) { + this.bi = (BufferedInputStream) bi; + } else { + this.bi = new BufferedInputStream(bi); + } + changeCipher(tc); + } + + public void changeCipher(BlockCipher bc) + { + this.currentCipher = bc; + blockSize = bc.getBlockSize(); + buffer = new byte[blockSize]; + enc = new byte[blockSize]; + pos = blockSize; + } + + private void getBlock() throws IOException + { + int n = 0; + while (n < blockSize) + { + int len = bi.read(enc, n, blockSize - n); + if (len < 0) + throw new IOException("Cannot read full block, EOF reached."); + n += len; + } + + try + { + currentCipher.transformBlock(enc, 0, buffer, 0); + } + catch (Exception e) + { + throw new IOException("Error while decrypting block."); + } + pos = 0; + } + + public int read(byte[] dst) throws IOException + { + return read(dst, 0, dst.length); + } + + public int read(byte[] dst, int off, int len) throws IOException + { + int count = 0; + + while (len > 0) + { + if (pos >= blockSize) + getBlock(); + + int avail = blockSize - pos; + int copy = Math.min(avail, len); + System.arraycopy(buffer, pos, dst, off, copy); + pos += copy; + off += copy; + len -= copy; + count += copy; + } + return count; + } + + public int read() throws IOException + { + if (pos >= blockSize) + { + getBlock(); + } + return buffer[pos++] & 0xff; + } + + public int readPlain(byte[] b, int off, int len) throws IOException + { + if (pos != blockSize) + throw new IOException("Cannot read plain since crypto buffer is not aligned."); + int n = 0; + while (n < len) + { + int cnt = bi.read(b, off + n, len - n); + if (cnt < 0) + throw new IOException("Cannot fill buffer, EOF reached."); + n += cnt; + } + return n; + } + + public int peekPlain(byte[] b, int off, int len) throws IOException + { + if (pos != blockSize) + throw new IOException("Cannot read plain since crypto buffer is not aligned."); + int n = 0; + + bi.mark(len); + try { + while (n < len) { + int cnt = bi.read(b, off + n, len - n); + if (cnt < 0) + throw new IOException("Cannot fill buffer, EOF reached."); + n += cnt; + } + } finally { + bi.reset(); + } + + return n; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java new file mode 100644 index 0000000000..b01ec3a45d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java @@ -0,0 +1,120 @@ + +package com.trilead.ssh2.crypto.cipher; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * CipherOutputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CipherOutputStream.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CipherOutputStream +{ + private BlockCipher currentCipher; + private final BufferedOutputStream bo; + private byte[] buffer; + private byte[] enc; + private int blockSize; + private int pos; + private boolean recordingOutput; + private final ByteArrayOutputStream recordingOutputStream = new ByteArrayOutputStream(); + + public CipherOutputStream(BlockCipher tc, OutputStream bo) + { + if (bo instanceof BufferedOutputStream) { + this.bo = (BufferedOutputStream) bo; + } else { + this.bo = new BufferedOutputStream(bo); + } + changeCipher(tc); + } + + public void flush() throws IOException + { + if (pos != 0) + throw new IOException("FATAL: cannot flush since crypto buffer is not aligned."); + + bo.flush(); + } + + public void changeCipher(BlockCipher bc) + { + this.currentCipher = bc; + blockSize = bc.getBlockSize(); + buffer = new byte[blockSize]; + enc = new byte[blockSize]; + pos = 0; + } + + public void startRecording() { + recordingOutput = true; + } + + public byte[] getRecordedOutput() { + recordingOutput = false; + byte[] recordedOutput = recordingOutputStream.toByteArray(); + recordingOutputStream.reset(); + return recordedOutput; + } + + private void writeBlock() throws IOException + { + try + { + currentCipher.transformBlock(buffer, 0, enc, 0); + } + catch (Exception e) + { + throw new IOException("Error while decrypting block.", e); + } + + bo.write(enc, 0, blockSize); + pos = 0; + + if (recordingOutput) { + recordingOutputStream.write(enc, 0, blockSize); + } + } + + public void write(byte[] src, int off, int len) throws IOException + { + while (len > 0) + { + int avail = blockSize - pos; + int copy = Math.min(avail, len); + + System.arraycopy(src, off, buffer, pos, copy); + pos += copy; + off += copy; + len -= copy; + + if (pos >= blockSize) + writeBlock(); + } + } + + public void write(int b) throws IOException + { + buffer[pos++] = (byte) b; + if (pos >= blockSize) + writeBlock(); + } + + public void writePlain(int b) throws IOException + { + if (pos != 0) + throw new IOException("Cannot write plain since crypto buffer is not aligned."); + bo.write(b); + } + + public void writePlain(byte[] b, int off, int len) throws IOException + { + if (pos != 0) + throw new IOException("Cannot write plain since crypto buffer is not aligned."); + bo.write(b, off, len); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java new file mode 100644 index 0000000000..81caa9944b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java @@ -0,0 +1,392 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + * This file is based on the 3DES implementation from the Bouncy Castle Crypto package. + * Their licence file states the following: + * + * Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + * (http://www.bouncycastle.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * DES. + * + * @author See comments in the source file + * @version $Id: DES.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class DES implements BlockCipher +{ + private int[] workingKey = null; + + /** + * standard constructor. + */ + public DES() + { + } + + /** + * initialise a DES cipher. + * + * @param encrypting + * whether or not we are for encryption. + * @param key + * the parameters required to set up the cipher. + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + @Override + public void init(boolean encrypting, byte[] key, byte[] iv) + { + this.workingKey = generateWorkingKey(encrypting, key, 0); + } + + public String getAlgorithmName() + { + return "DES"; + } + + @Override + public int getBlockSize() + { + return 8; + } + + public void transformBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (workingKey == null) + { + throw new IllegalStateException("DES engine not initialised!"); + } + + desFunc(workingKey, in, inOff, out, outOff); + } + + /** + * what follows is mainly taken from "Applied Cryptography", by Bruce + * Schneier, however it also bears great resemblance to Richard + * Outerbridge's D3DES... + */ + + static short[] Df_Key = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, + 0x10, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67 }; + + static short[] bytebit = { 0200, 0100, 040, 020, 010, 04, 02, 01 }; + + static int[] bigbyte = { 0x800000, 0x400000, 0x200000, 0x100000, 0x80000, 0x40000, 0x20000, 0x10000, 0x8000, + 0x4000, 0x2000, 0x1000, 0x800, 0x400, 0x200, 0x100, 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1 }; + + /* + * Use the key schedule specified in the Standard (ANSI X3.92-1981). + */ + + static byte[] pc1 = { 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, + 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, + 4, 27, 19, 11, 3 }; + + static byte[] totrot = { 1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28 }; + + static byte[] pc2 = { 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, + 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31 }; + + static int[] SP1 = { 0x01010400, 0x00000000, 0x00010000, 0x01010404, 0x01010004, 0x00010404, 0x00000004, + 0x00010000, 0x00000400, 0x01010400, 0x01010404, 0x00000400, 0x01000404, 0x01010004, 0x01000000, 0x00000004, + 0x00000404, 0x01000400, 0x01000400, 0x00010400, 0x00010400, 0x01010000, 0x01010000, 0x01000404, 0x00010004, + 0x01000004, 0x01000004, 0x00010004, 0x00000000, 0x00000404, 0x00010404, 0x01000000, 0x00010000, 0x01010404, + 0x00000004, 0x01010000, 0x01010400, 0x01000000, 0x01000000, 0x00000400, 0x01010004, 0x00010000, 0x00010400, + 0x01000004, 0x00000400, 0x00000004, 0x01000404, 0x00010404, 0x01010404, 0x00010004, 0x01010000, 0x01000404, + 0x01000004, 0x00000404, 0x00010404, 0x01010400, 0x00000404, 0x01000400, 0x01000400, 0x00000000, 0x00010004, + 0x00010400, 0x00000000, 0x01010004 }; + + static int[] SP2 = { 0x80108020, 0x80008000, 0x00008000, 0x00108020, 0x00100000, 0x00000020, 0x80100020, + 0x80008020, 0x80000020, 0x80108020, 0x80108000, 0x80000000, 0x80008000, 0x00100000, 0x00000020, 0x80100020, + 0x00108000, 0x00100020, 0x80008020, 0x00000000, 0x80000000, 0x00008000, 0x00108020, 0x80100000, 0x00100020, + 0x80000020, 0x00000000, 0x00108000, 0x00008020, 0x80108000, 0x80100000, 0x00008020, 0x00000000, 0x00108020, + 0x80100020, 0x00100000, 0x80008020, 0x80100000, 0x80108000, 0x00008000, 0x80100000, 0x80008000, 0x00000020, + 0x80108020, 0x00108020, 0x00000020, 0x00008000, 0x80000000, 0x00008020, 0x80108000, 0x00100000, 0x80000020, + 0x00100020, 0x80008020, 0x80000020, 0x00100020, 0x00108000, 0x00000000, 0x80008000, 0x00008020, 0x80000000, + 0x80100020, 0x80108020, 0x00108000 }; + + static int[] SP3 = { 0x00000208, 0x08020200, 0x00000000, 0x08020008, 0x08000200, 0x00000000, 0x00020208, + 0x08000200, 0x00020008, 0x08000008, 0x08000008, 0x00020000, 0x08020208, 0x00020008, 0x08020000, 0x00000208, + 0x08000000, 0x00000008, 0x08020200, 0x00000200, 0x00020200, 0x08020000, 0x08020008, 0x00020208, 0x08000208, + 0x00020200, 0x00020000, 0x08000208, 0x00000008, 0x08020208, 0x00000200, 0x08000000, 0x08020200, 0x08000000, + 0x00020008, 0x00000208, 0x00020000, 0x08020200, 0x08000200, 0x00000000, 0x00000200, 0x00020008, 0x08020208, + 0x08000200, 0x08000008, 0x00000200, 0x00000000, 0x08020008, 0x08000208, 0x00020000, 0x08000000, 0x08020208, + 0x00000008, 0x00020208, 0x00020200, 0x08000008, 0x08020000, 0x08000208, 0x00000208, 0x08020000, 0x00020208, + 0x00000008, 0x08020008, 0x00020200 }; + + static int[] SP4 = { 0x00802001, 0x00002081, 0x00002081, 0x00000080, 0x00802080, 0x00800081, 0x00800001, + 0x00002001, 0x00000000, 0x00802000, 0x00802000, 0x00802081, 0x00000081, 0x00000000, 0x00800080, 0x00800001, + 0x00000001, 0x00002000, 0x00800000, 0x00802001, 0x00000080, 0x00800000, 0x00002001, 0x00002080, 0x00800081, + 0x00000001, 0x00002080, 0x00800080, 0x00002000, 0x00802080, 0x00802081, 0x00000081, 0x00800080, 0x00800001, + 0x00802000, 0x00802081, 0x00000081, 0x00000000, 0x00000000, 0x00802000, 0x00002080, 0x00800080, 0x00800081, + 0x00000001, 0x00802001, 0x00002081, 0x00002081, 0x00000080, 0x00802081, 0x00000081, 0x00000001, 0x00002000, + 0x00800001, 0x00002001, 0x00802080, 0x00800081, 0x00002001, 0x00002080, 0x00800000, 0x00802001, 0x00000080, + 0x00800000, 0x00002000, 0x00802080 }; + + static int[] SP5 = { 0x00000100, 0x02080100, 0x02080000, 0x42000100, 0x00080000, 0x00000100, 0x40000000, + 0x02080000, 0x40080100, 0x00080000, 0x02000100, 0x40080100, 0x42000100, 0x42080000, 0x00080100, 0x40000000, + 0x02000000, 0x40080000, 0x40080000, 0x00000000, 0x40000100, 0x42080100, 0x42080100, 0x02000100, 0x42080000, + 0x40000100, 0x00000000, 0x42000000, 0x02080100, 0x02000000, 0x42000000, 0x00080100, 0x00080000, 0x42000100, + 0x00000100, 0x02000000, 0x40000000, 0x02080000, 0x42000100, 0x40080100, 0x02000100, 0x40000000, 0x42080000, + 0x02080100, 0x40080100, 0x00000100, 0x02000000, 0x42080000, 0x42080100, 0x00080100, 0x42000000, 0x42080100, + 0x02080000, 0x00000000, 0x40080000, 0x42000000, 0x00080100, 0x02000100, 0x40000100, 0x00080000, 0x00000000, + 0x40080000, 0x02080100, 0x40000100 }; + + static int[] SP6 = { 0x20000010, 0x20400000, 0x00004000, 0x20404010, 0x20400000, 0x00000010, 0x20404010, + 0x00400000, 0x20004000, 0x00404010, 0x00400000, 0x20000010, 0x00400010, 0x20004000, 0x20000000, 0x00004010, + 0x00000000, 0x00400010, 0x20004010, 0x00004000, 0x00404000, 0x20004010, 0x00000010, 0x20400010, 0x20400010, + 0x00000000, 0x00404010, 0x20404000, 0x00004010, 0x00404000, 0x20404000, 0x20000000, 0x20004000, 0x00000010, + 0x20400010, 0x00404000, 0x20404010, 0x00400000, 0x00004010, 0x20000010, 0x00400000, 0x20004000, 0x20000000, + 0x00004010, 0x20000010, 0x20404010, 0x00404000, 0x20400000, 0x00404010, 0x20404000, 0x00000000, 0x20400010, + 0x00000010, 0x00004000, 0x20400000, 0x00404010, 0x00004000, 0x00400010, 0x20004010, 0x00000000, 0x20404000, + 0x20000000, 0x00400010, 0x20004010 }; + + static int[] SP7 = { 0x00200000, 0x04200002, 0x04000802, 0x00000000, 0x00000800, 0x04000802, 0x00200802, + 0x04200800, 0x04200802, 0x00200000, 0x00000000, 0x04000002, 0x00000002, 0x04000000, 0x04200002, 0x00000802, + 0x04000800, 0x00200802, 0x00200002, 0x04000800, 0x04000002, 0x04200000, 0x04200800, 0x00200002, 0x04200000, + 0x00000800, 0x00000802, 0x04200802, 0x00200800, 0x00000002, 0x04000000, 0x00200800, 0x04000000, 0x00200800, + 0x00200000, 0x04000802, 0x04000802, 0x04200002, 0x04200002, 0x00000002, 0x00200002, 0x04000000, 0x04000800, + 0x00200000, 0x04200800, 0x00000802, 0x00200802, 0x04200800, 0x00000802, 0x04000002, 0x04200802, 0x04200000, + 0x00200800, 0x00000000, 0x00000002, 0x04200802, 0x00000000, 0x00200802, 0x04200000, 0x00000800, 0x04000002, + 0x04000800, 0x00000800, 0x00200002 }; + + static int[] SP8 = { 0x10001040, 0x00001000, 0x00040000, 0x10041040, 0x10000000, 0x10001040, 0x00000040, + 0x10000000, 0x00040040, 0x10040000, 0x10041040, 0x00041000, 0x10041000, 0x00041040, 0x00001000, 0x00000040, + 0x10040000, 0x10000040, 0x10001000, 0x00001040, 0x00041000, 0x00040040, 0x10040040, 0x10041000, 0x00001040, + 0x00000000, 0x00000000, 0x10040040, 0x10000040, 0x10001000, 0x00041040, 0x00040000, 0x00041040, 0x00040000, + 0x10041000, 0x00001000, 0x00000040, 0x10040040, 0x00001000, 0x00041040, 0x10001000, 0x00000040, 0x10000040, + 0x10040000, 0x10040040, 0x10000000, 0x00040000, 0x10001040, 0x00000000, 0x10041040, 0x00040040, 0x10000040, + 0x10040000, 0x10001000, 0x10001040, 0x00000000, 0x10041040, 0x00041000, 0x00041000, 0x00001040, 0x00001040, + 0x00040040, 0x10000000, 0x10041000 }; + + /** + * generate an integer based working key based on our secret key and what we + * processing we are planning to do. + * + * Acknowledgements for this routine go to James Gillogly & Phil Karn. + * (whoever, and wherever they are!). + */ + protected int[] generateWorkingKey(boolean encrypting, byte[] key, int off) + { + int[] newKey = new int[32]; + boolean[] pc1m = new boolean[56], pcr = new boolean[56]; + + for (int j = 0; j < 56; j++) + { + int l = pc1[j]; + + pc1m[j] = ((key[off + (l >>> 3)] & bytebit[l & 07]) != 0); + } + + for (int i = 0; i < 16; i++) + { + int l, m, n; + + if (encrypting) + { + m = i << 1; + } + else + { + m = (15 - i) << 1; + } + + n = m + 1; + newKey[m] = newKey[n] = 0; + + for (int j = 0; j < 28; j++) + { + l = j + totrot[i]; + if (l < 28) + { + pcr[j] = pc1m[l]; + } + else + { + pcr[j] = pc1m[l - 28]; + } + } + + for (int j = 28; j < 56; j++) + { + l = j + totrot[i]; + if (l < 56) + { + pcr[j] = pc1m[l]; + } + else + { + pcr[j] = pc1m[l - 28]; + } + } + + for (int j = 0; j < 24; j++) + { + if (pcr[pc2[j]]) + { + newKey[m] |= bigbyte[j]; + } + + if (pcr[pc2[j + 24]]) + { + newKey[n] |= bigbyte[j]; + } + } + } + + // + // store the processed key + // + for (int i = 0; i != 32; i += 2) + { + int i1, i2; + + i1 = newKey[i]; + i2 = newKey[i + 1]; + + newKey[i] = ((i1 & 0x00fc0000) << 6) | ((i1 & 0x00000fc0) << 10) | ((i2 & 0x00fc0000) >>> 10) + | ((i2 & 0x00000fc0) >>> 6); + + newKey[i + 1] = ((i1 & 0x0003f000) << 12) | ((i1 & 0x0000003f) << 16) | ((i2 & 0x0003f000) >>> 4) + | (i2 & 0x0000003f); + } + + return newKey; + } + + /** + * the DES engine. + */ + protected void desFunc(int[] wKey, byte[] in, int inOff, byte[] out, int outOff) + { + int work, right, left; + + left = (in[inOff + 0] & 0xff) << 24; + left |= (in[inOff + 1] & 0xff) << 16; + left |= (in[inOff + 2] & 0xff) << 8; + left |= (in[inOff + 3] & 0xff); + + right = (in[inOff + 4] & 0xff) << 24; + right |= (in[inOff + 5] & 0xff) << 16; + right |= (in[inOff + 6] & 0xff) << 8; + right |= (in[inOff + 7] & 0xff); + + work = ((left >>> 4) ^ right) & 0x0f0f0f0f; + right ^= work; + left ^= (work << 4); + work = ((left >>> 16) ^ right) & 0x0000ffff; + right ^= work; + left ^= (work << 16); + work = ((right >>> 2) ^ left) & 0x33333333; + left ^= work; + right ^= (work << 2); + work = ((right >>> 8) ^ left) & 0x00ff00ff; + left ^= work; + right ^= (work << 8); + right = ((right << 1) | ((right >>> 31) & 1)) & 0xffffffff; + work = (left ^ right) & 0xaaaaaaaa; + left ^= work; + right ^= work; + left = ((left << 1) | ((left >>> 31) & 1)) & 0xffffffff; + + for (int round = 0; round < 8; round++) + { + int fval; + + work = (right << 28) | (right >>> 4); + work ^= wKey[round * 4 + 0]; + fval = SP7[work & 0x3f]; + fval |= SP5[(work >>> 8) & 0x3f]; + fval |= SP3[(work >>> 16) & 0x3f]; + fval |= SP1[(work >>> 24) & 0x3f]; + work = right ^ wKey[round * 4 + 1]; + fval |= SP8[work & 0x3f]; + fval |= SP6[(work >>> 8) & 0x3f]; + fval |= SP4[(work >>> 16) & 0x3f]; + fval |= SP2[(work >>> 24) & 0x3f]; + left ^= fval; + work = (left << 28) | (left >>> 4); + work ^= wKey[round * 4 + 2]; + fval = SP7[work & 0x3f]; + fval |= SP5[(work >>> 8) & 0x3f]; + fval |= SP3[(work >>> 16) & 0x3f]; + fval |= SP1[(work >>> 24) & 0x3f]; + work = left ^ wKey[round * 4 + 3]; + fval |= SP8[work & 0x3f]; + fval |= SP6[(work >>> 8) & 0x3f]; + fval |= SP4[(work >>> 16) & 0x3f]; + fval |= SP2[(work >>> 24) & 0x3f]; + right ^= fval; + } + + right = (right << 31) | (right >>> 1); + work = (left ^ right) & 0xaaaaaaaa; + left ^= work; + right ^= work; + left = (left << 31) | (left >>> 1); + work = ((left >>> 8) ^ right) & 0x00ff00ff; + right ^= work; + left ^= (work << 8); + work = ((left >>> 2) ^ right) & 0x33333333; + right ^= work; + left ^= (work << 2); + work = ((right >>> 16) ^ left) & 0x0000ffff; + left ^= work; + right ^= (work << 16); + work = ((right >>> 4) ^ left) & 0x0f0f0f0f; + left ^= work; + right ^= (work << 4); + + out[outOff + 0] = (byte) ((right >>> 24) & 0xff); + out[outOff + 1] = (byte) ((right >>> 16) & 0xff); + out[outOff + 2] = (byte) ((right >>> 8) & 0xff); + out[outOff + 3] = (byte) (right & 0xff); + out[outOff + 4] = (byte) ((left >>> 24) & 0xff); + out[outOff + 5] = (byte) ((left >>> 16) & 0xff); + out[outOff + 6] = (byte) ((left >>> 8) & 0xff); + out[outOff + 7] = (byte) (left & 0xff); + } + + public static class CBC implements BlockCipher { + protected BlockCipher bc; + + @Override + public void init(boolean forEncryption, byte[] key, byte[] iv) throws IllegalArgumentException { + BlockCipher rawCipher = new DESede(); + rawCipher.init(forEncryption, key, iv); + bc = new CBCMode(rawCipher, iv, forEncryption); + } + + @Override + public int getBlockSize() { + return bc.getBlockSize(); + } + + @Override + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) { + bc.transformBlock(src, srcoff, dst, dstoff); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java new file mode 100644 index 0000000000..480a7fe640 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java @@ -0,0 +1,130 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + * This file was shamelessly taken (and modified) from the Bouncy Castle Crypto package. + * Their licence file states the following: + * + * Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + * (http://www.bouncycastle.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * DESede. + * + * @author See comments in the source file + * @version $Id: DESede.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class DESede extends DES +{ + private int[] key1 = null; + private int[] key2 = null; + private int[] key3 = null; + + private boolean encrypt; + + /** + * standard constructor. + */ + public DESede() + { + } + + /** + * initialise a DES cipher. + * + * @param encrypting + * whether or not we are for encryption. + * @param key + * the parameters required to set up the cipher. + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + @Override + public void init(boolean encrypting, byte[] key, byte[] iv) + { + key1 = generateWorkingKey(encrypting, key, 0); + key2 = generateWorkingKey(!encrypting, key, 8); + key3 = generateWorkingKey(encrypting, key, 16); + + encrypt = encrypting; + } + + public String getAlgorithmName() + { + return "DESede"; + } + + @Override + public void transformBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (key1 == null) + { + throw new IllegalStateException("DESede engine not initialised!"); + } + + if (encrypt) + { + desFunc(key1, in, inOff, out, outOff); + desFunc(key2, out, outOff, out, outOff); + desFunc(key3, out, outOff, out, outOff); + } + else + { + desFunc(key3, in, inOff, out, outOff); + desFunc(key2, out, outOff, out, outOff); + desFunc(key1, out, outOff, out, outOff); + } + } + + private abstract static class Wrapper implements BlockCipher { + protected BlockCipher bc; + + @Override + public int getBlockSize() { + return bc.getBlockSize(); + } + + @Override + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) { + bc.transformBlock(src, srcoff, dst, dstoff); + } + } + + public static class CBC extends Wrapper { + @Override + public void init(boolean forEncryption, byte[] key, byte[] iv) throws IllegalArgumentException { + BlockCipher rawCipher = new DESede(); + rawCipher.init(forEncryption, key, iv); + bc = new CBCMode(rawCipher, iv, forEncryption); + } + } + + public static class CTR extends Wrapper { + @Override + public void init(boolean forEncryption, byte[] key, byte[] iv) throws IllegalArgumentException { + BlockCipher rawCipher = new DESede(); + rawCipher.init(true, key, iv); + bc = new CTRMode(rawCipher, iv, forEncryption); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/EtmCipher.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/EtmCipher.java new file mode 100644 index 0000000000..174b20e0b6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/EtmCipher.java @@ -0,0 +1,4 @@ +package com.trilead.ssh2.crypto.cipher; + +public interface EtmCipher { +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java new file mode 100644 index 0000000000..4617a3b081 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java @@ -0,0 +1,35 @@ +package com.trilead.ssh2.crypto.cipher; + +/** + * NullCipher. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NullCipher.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class NullCipher implements BlockCipher +{ + private int blockSize = 8; + + public NullCipher() + { + } + + public NullCipher(int blockSize) + { + this.blockSize = blockSize; + } + + public void init(boolean forEncryption, byte[] key, byte[] iv) + { + } + + public int getBlockSize() + { + return blockSize; + } + + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + System.arraycopy(src, srcoff, dst, dstoff, blockSize); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java new file mode 100644 index 0000000000..01d4ab472a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/Curve25519Exchange.java @@ -0,0 +1,85 @@ +package com.trilead.ssh2.crypto.dh; + +import com.google.crypto.tink.subtle.X25519; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; + +/** + * Created by Kenny Root on 1/23/16. + */ +public class Curve25519Exchange extends GenericDhExchange { + public static final String NAME = "curve25519-sha256"; + public static final String ALT_NAME = "curve25519-sha256@libssh.org"; + public static final int KEY_SIZE = 32; + + private byte[] clientPublic; + private byte[] clientPrivate; + private byte[] serverPublic; + + public Curve25519Exchange() { + super(); + } + + /* + * Used to test known vectors. + */ + public Curve25519Exchange(byte[] secret) throws InvalidKeyException { + if (secret.length != KEY_SIZE) { + throw new AssertionError("secret must be key size"); + } + clientPrivate = secret.clone(); + } + + @Override + public void init(String name) throws IOException { + if (!NAME.equals(name) && !ALT_NAME.equals(name)) { + throw new IOException("Invalid name " + name); + } + + clientPrivate = X25519.generatePrivateKey(); + try { + clientPublic = X25519.publicFromPrivate(clientPrivate); + } catch (InvalidKeyException e) { + throw new IOException(e); + } + } + + @Override + public byte[] getE() { + return clientPublic.clone(); + } + + @Override + protected byte[] getServerE() { + return serverPublic.clone(); + } + + @Override + public void setF(byte[] f) throws IOException { + if (f.length != KEY_SIZE) { + throw new IOException("Server sent invalid key length " + f.length + " (expected " + + KEY_SIZE + ")"); + } + serverPublic = f.clone(); + try { + byte[] sharedSecretBytes = X25519.computeSharedSecret(clientPrivate, serverPublic); + int allBytes = 0; + for (int i = 0; i < sharedSecretBytes.length; i++) { + allBytes |= sharedSecretBytes[i]; + } + if (allBytes == 0) { + throw new IOException("Invalid key computed; all zeroes"); + } + sharedSecret = new BigInteger(1, sharedSecretBytes); + } catch (InvalidKeyException e) { + throw new IOException(e); + } + } + + @Override + public String getHashAlgo() { + return "SHA-256"; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java new file mode 100644 index 0000000000..08b82eef91 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java @@ -0,0 +1,212 @@ +/** + * + */ +package com.trilead.ssh2.crypto.dh; + +import javax.crypto.KeyAgreement; +import javax.crypto.interfaces.DHPrivateKey; +import javax.crypto.interfaces.DHPublicKey; +import javax.crypto.spec.DHParameterSpec; +import javax.crypto.spec.DHPublicKeySpec; +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +/** + * @author kenny + * + */ +public class DhExchange extends GenericDhExchange { + + /* Given by the standard */ + + private static final BigInteger P1 = new BigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" + + "FFFFFFFFFFFFFFFF", 16); + + private static final BigInteger P14 = new BigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16); + + private static final BigInteger P16 = new BigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" + + "FFFFFFFFFFFFFFFF", 16); + + private static final BigInteger P18 = new BigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492" + + "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BD" + + "F8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831" + + "179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B" + + "DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF" + + "5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6" + + "D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F3" + + "23A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA" + + "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE328" + + "06A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55C" + + "DA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE" + + "12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E4" + + "38777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300" + + "741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F568" + + "3423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9" + + "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B" + + "4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A" + + "062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A36" + + "4597E899A0255DC164F31CC50846851DF9AB48195DED7EA1" + + "B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F92" + + "4009438B481C6CD7889A002ED5EE382BC9190DA6FC026E47" + + "9558E4475677E9AA9E3050E2765694DFC81F56E880B96E71" + + "60C980DD98EDD3DFFFFFFFFFFFFFFFFF", 16); + + private static final BigInteger G = BigInteger.valueOf(2); + + /* Hash algorithm to use */ + private String hashAlgo; + + /* Client public and private */ + + private DHPrivateKey clientPrivate; + private DHPublicKey clientPublic; + + /* Server public */ + + private DHPublicKey serverPublic; + + @Override + public void init(String name) throws IOException { + final DHParameterSpec spec; + if ("diffie-hellman-group18-sha512".equals(name)) { + spec = new DHParameterSpec(P18, G); + hashAlgo = "SHA-512"; + } else if ("diffie-hellman-group16-sha512".equals(name)) { + spec = new DHParameterSpec(P16, G); + hashAlgo = "SHA-512"; + } else if ("diffie-hellman-group14-sha256".equals(name)) { + spec = new DHParameterSpec(P14, G); + hashAlgo = "SHA-256"; + } else if ("diffie-hellman-group14-sha1".equals(name)) { + spec = new DHParameterSpec(P14, G); + hashAlgo = "SHA-1"; + } else if ("diffie-hellman-group1-sha1".equals(name)) { + spec = new DHParameterSpec(P1, G); + hashAlgo = "SHA-1"; + } else { + throw new IllegalArgumentException("Unknown DH group " + name); + } + + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("DH"); + kpg.initialize(spec); + KeyPair pair = kpg.generateKeyPair(); + clientPrivate = (DHPrivateKey) pair.getPrivate(); + clientPublic = (DHPublicKey) pair.getPublic(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("No DH keypair generator", e); + } catch (InvalidAlgorithmParameterException e) { + throw new IOException("Invalid DH parameters", e); + } + } + + @Override + public byte[] getE() { + if (clientPublic == null) + throw new IllegalStateException("DhExchange not initialized!"); + + return clientPublic.getY().toByteArray(); + } + + @Override + protected byte[] getServerE() { + if (serverPublic == null) + throw new IllegalStateException("DhExchange not initialized!"); + + return serverPublic.getY().toByteArray(); + } + + @Override + public void setF(byte[] f) throws IOException { + if (clientPublic == null) + throw new IllegalStateException("DhExchange not initialized!"); + + final KeyAgreement ka; + try { + KeyFactory kf = KeyFactory.getInstance("DH"); + DHParameterSpec params = clientPublic.getParams(); + this.serverPublic = (DHPublicKey) kf.generatePublic(new DHPublicKeySpec( + new BigInteger(1, f), params.getP(), params.getG())); + + ka = KeyAgreement.getInstance("DH"); + ka.init(clientPrivate); + ka.doPhase(serverPublic, true); + } catch (NoSuchAlgorithmException e) { + throw new IOException("No DH key agreement method", e); + } catch (InvalidKeyException | InvalidKeySpecException e) { + throw new IOException("Invalid DH key", e); + } + + sharedSecret = new BigInteger(1, ka.generateSecret()); + } + + @Override + public String getHashAlgo() { + return hashAlgo; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java new file mode 100644 index 0000000000..1356c57e23 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java @@ -0,0 +1,113 @@ + +package com.trilead.ssh2.crypto.dh; + +import java.math.BigInteger; +import java.security.SecureRandom; + +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.crypto.digest.HashForSSH2Types; + + +/** + * DhGroupExchange. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DhGroupExchange.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class DhGroupExchange +{ + /* Given by the standard */ + + private BigInteger p; + private BigInteger g; + + /* Client public and private */ + + private BigInteger e; + private BigInteger x; + + /* Server public */ + + private BigInteger f; + + /* Shared secret */ + + private BigInteger k; + + public DhGroupExchange(BigInteger p, BigInteger g) + { + this.p = p; + this.g = g; + } + + public void init(SecureRandom rnd) + { + k = null; + + x = new BigInteger(p.bitLength() - 1, rnd); + e = g.modPow(x, p); + } + + /** + * @return Returns the e. + */ + public BigInteger getE() + { + if (e == null) + throw new IllegalStateException("Not initialized!"); + + return e; + } + + /** + * @return Returns the shared secret k. + */ + public BigInteger getK() + { + if (k == null) + throw new IllegalStateException("Shared secret not yet known, need f first!"); + + return k; + } + + /** + * Sets f and calculates the shared secret. + */ + public void setF(BigInteger f) + { + if (e == null) + throw new IllegalStateException("Not initialized!"); + + BigInteger zero = BigInteger.valueOf(0); + + if (zero.compareTo(f) >= 0 || p.compareTo(f) <= 0) + throw new IllegalArgumentException("Invalid f specified!"); + + this.f = f; + this.k = f.modPow(x, p); + } + + public byte[] calculateH(String hashAlgo, byte[] clientversion, byte[] serverversion, + byte[] clientKexPayload, byte[] serverKexPayload, byte[] hostKey, DHGexParameters para) + { + HashForSSH2Types hash = new HashForSSH2Types(hashAlgo); + + hash.updateByteString(clientversion); + hash.updateByteString(serverversion); + hash.updateByteString(clientKexPayload); + hash.updateByteString(serverKexPayload); + hash.updateByteString(hostKey); + if (para.getMin_group_len() > 0) + hash.updateUINT32(para.getMin_group_len()); + hash.updateUINT32(para.getPref_group_len()); + if (para.getMax_group_len() > 0) + hash.updateUINT32(para.getMax_group_len()); + hash.updateBigInt(p); + hash.updateBigInt(g); + hash.updateBigInt(e); + hash.updateBigInt(f); + hash.updateBigInt(k); + + return hash.getDigest(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java new file mode 100644 index 0000000000..e6e30f6a38 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java @@ -0,0 +1,106 @@ +package com.trilead.ssh2.crypto.dh; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.KeyAgreement; + +import com.trilead.ssh2.signature.ECDSASHA2Verify; + +/** + * @author kenny + * + */ +public class EcDhExchange extends GenericDhExchange { + private ECPrivateKey clientPrivate; + private ECPublicKey clientPublic; + private ECPublicKey serverPublic; + + @Override + public void init(String name) throws IOException { + final ECParameterSpec spec; + + if ("ecdh-sha2-nistp256".equals(name)) { + spec = ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().getParameterSpec(); + } else if ("ecdh-sha2-nistp384".equals(name)) { + spec = ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().getParameterSpec(); + } else if ("ecdh-sha2-nistp521".equals(name)) { + spec = ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().getParameterSpec(); + } else { + throw new IllegalArgumentException("Unknown EC curve " + name); + } + + KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(spec); + KeyPair pair = kpg.generateKeyPair(); + clientPrivate = (ECPrivateKey) pair.getPrivate(); + clientPublic = (ECPublicKey) pair.getPublic(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("No DH keypair generator", e); + } catch (InvalidAlgorithmParameterException e) { + throw new IOException("Invalid DH parameters", e); + } + } + + @Override + public byte[] getE() { + return ECDSASHA2Verify.encodeECPoint(clientPublic.getW(), clientPublic.getParams() + .getCurve()); + } + + @Override + protected byte[] getServerE() { + return ECDSASHA2Verify.encodeECPoint(serverPublic.getW(), serverPublic.getParams() + .getCurve()); + } + + @Override + public void setF(byte[] f) throws IOException { + + if (clientPublic == null) + throw new IllegalStateException("DhDsaExchange not initialized!"); + + final KeyAgreement ka; + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + ECDSASHA2Verify verifier = ECDSASHA2Verify.getVerifierForKey(clientPublic); + if (verifier == null) { + throw new IOException("No such EC group"); + } + + ECPoint serverPoint = verifier.decodeECPoint(f); + ECParameterSpec params = verifier.getParameterSpec(); + this.serverPublic = (ECPublicKey) kf.generatePublic(new ECPublicKeySpec(serverPoint, + params)); + + ka = KeyAgreement.getInstance("ECDH"); + ka.init(clientPrivate); + ka.doPhase(serverPublic, true); + } catch (NoSuchAlgorithmException e) { + throw new IOException("No ECDH key agreement method", e); + } catch (InvalidKeyException | InvalidKeySpecException e) { + throw new IOException("Invalid ECDH key", e); + } + + sharedSecret = new BigInteger(1, ka.generateSecret()); + } + + @Override + public String getHashAlgo() { + return ECDSASHA2Verify.getDigestAlgorithmForParams(clientPublic); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java new file mode 100644 index 0000000000..0ea0ab1e12 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java @@ -0,0 +1,96 @@ + +package com.trilead.ssh2.crypto.dh; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +import com.trilead.ssh2.crypto.digest.HashForSSH2Types; +import com.trilead.ssh2.log.Logger; + + +/** + * DhExchange. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DhExchange.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public abstract class GenericDhExchange +{ + private static final Logger log = Logger.getLogger(GenericDhExchange.class); + + /* Shared secret */ + + BigInteger sharedSecret; + + protected GenericDhExchange() + { + } + + public static GenericDhExchange getInstance(String algo) { + if (Curve25519Exchange.NAME.equals(algo) || Curve25519Exchange.ALT_NAME.equals(algo)) { + return new Curve25519Exchange(); + } + if (algo.startsWith("ecdh-sha2-")) { + return new EcDhExchange(); + } else { + return new DhExchange(); + } + } + + public abstract void init(String name) throws IOException; + + /** + * @return Returns the e (public value) + * @throws IllegalStateException + */ + public abstract byte[] getE(); + + /** + * @return Returns the server's e (public value) + * @throws IllegalStateException + */ + protected abstract byte[] getServerE(); + + /** + * @return Returns the shared secret k. + * @throws IllegalStateException + */ + public BigInteger getK() + { + if (sharedSecret == null) + throw new IllegalStateException("Shared secret not yet known, need f first!"); + + return sharedSecret; + } + + /** + * @param f + */ + public abstract void setF(byte[] f) throws IOException; + + public byte[] calculateH(byte[] clientversion, byte[] serverversion, byte[] clientKexPayload, + byte[] serverKexPayload, byte[] hostKey) throws UnsupportedEncodingException + { + HashForSSH2Types hash = new HashForSSH2Types(getHashAlgo()); + + if (log.isEnabled()) + { + log.log(90, "Client: '" + new String(clientversion) + "'"); + log.log(90, "Server: '" + new String(serverversion) + "'"); + } + + hash.updateByteString(clientversion); + hash.updateByteString(serverversion); + hash.updateByteString(clientKexPayload); + hash.updateByteString(serverKexPayload); + hash.updateByteString(hostKey); + hash.updateByteString(getE()); + hash.updateByteString(getServerE()); + hash.updateBigInt(sharedSecret); + + return hash.getDigest(); + } + + public abstract String getHashAlgo(); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HMAC.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HMAC.java new file mode 100644 index 0000000000..c6ee1293e8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HMAC.java @@ -0,0 +1,166 @@ + +package com.trilead.ssh2.crypto.digest; + +import javax.crypto.Mac; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * MAC. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: MAC.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public final class HMAC implements MAC +{ + private static final String ETM_SUFFIX = "-etm@openssh.com"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + static final String HMAC_MD5 = "hmac-md5"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + static final String HMAC_MD5_96 = "hmac-md5-96"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + static final String HMAC_SHA1 = "hmac-sha1"; + + /** + * From https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL + */ + static final String HMAC_SHA1_ETM = "hmac-sha1-etm@openssh.com"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + static final String HMAC_SHA1_96 = "hmac-sha1-96"; + + /** + * From https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL + */ + static final String HMAC_SHA2_256_ETM = "hmac-sha2-256-etm@openssh.com"; + + /** + * From https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL + */ + static final String HMAC_SHA2_512_ETM = "hmac-sha2-512-etm@openssh.com"; + + /** + * From http://tools.ietf.org/html/rfc6668 + */ + static final String HMAC_SHA2_256 = "hmac-sha2-256"; + + /** + * From http://tools.ietf.org/html/rfc6668 + */ + static final String HMAC_SHA2_512 = "hmac-sha2-512"; + + private final Mac mac; + private final int outSize; + private final boolean encryptThenMac; + private final byte[] buffer; + + public HMAC(String type, byte[] key) + { + try { + if (HMAC_SHA1.equals(type) || HMAC_SHA1_96.equals(type)) + { + mac = Mac.getInstance("HmacSHA1"); + encryptThenMac = false; + } + else if (HMAC_SHA1_ETM.equals(type)) + { + mac = Mac.getInstance("HmacSHA1"); + encryptThenMac = true; + } + else if (HMAC_MD5.equals(type) || HMAC_MD5_96.equals(type)) + { + mac = Mac.getInstance("HmacMD5"); + encryptThenMac = false; + } + else if (HMAC_SHA2_256.equals(type)) + { + mac = Mac.getInstance("HmacSHA256"); + encryptThenMac = false; + } + else if (HMAC_SHA2_256_ETM.equals(type)) + { + mac = Mac.getInstance("HmacSHA256"); + encryptThenMac = true; + } + else if (HMAC_SHA2_512.equals(type)) + { + mac = Mac.getInstance("HmacSHA512"); + encryptThenMac = false; + } + else if (HMAC_SHA2_512_ETM.equals(type)) + { + mac = Mac.getInstance("HmacSHA512"); + encryptThenMac = true; + } + else + throw new IllegalArgumentException("Unknown algorithm " + type); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unknown algorithm " + type, e); + } + + int macSize = mac.getMacLength(); + if (type.endsWith("-96")) { + outSize = 12; + buffer = new byte[macSize]; + } else { + outSize = macSize; + buffer = null; + } + + try { + mac.init(new SecretKeySpec(key, type)); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(e); + } + } + + public final void initMac(int seq) + { + mac.reset(); + mac.update((byte) (seq >> 24)); + mac.update((byte) (seq >> 16)); + mac.update((byte) (seq >> 8)); + mac.update((byte) (seq)); + } + + public final void update(byte[] packetdata, int off, int len) + { + mac.update(packetdata, off, len); + } + + public final void getMac(byte[] out, int off) + { + try { + if (buffer != null) { + mac.doFinal(buffer, 0); + System.arraycopy(buffer, 0, out, off, out.length - off); + } else { + mac.doFinal(out, off); + } + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + } + + public final int size() + { + return outSize; + } + + public boolean isEncryptThenMac() { + return encryptThenMac; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java new file mode 100644 index 0000000000..fad40efdc3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java @@ -0,0 +1,91 @@ + +package com.trilead.ssh2.crypto.digest; + +import java.math.BigInteger; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * HashForSSH2Types. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: HashForSSH2Types.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class HashForSSH2Types +{ + MessageDigest md; + + public HashForSSH2Types(String type) + { + try { + md = MessageDigest.getInstance(type); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unsupported algorithm " + type); + } + } + + public void updateByte(byte b) + { + /* HACK - to test it with J2ME */ + byte[] tmp = new byte[1]; + tmp[0] = b; + md.update(tmp); + } + + public void updateBytes(byte[] b) + { + md.update(b); + } + + public void updateUINT32(int v) + { + md.update((byte) (v >> 24)); + md.update((byte) (v >> 16)); + md.update((byte) (v >> 8)); + md.update((byte) (v)); + } + + public void updateByteString(byte[] b) + { + updateUINT32(b.length); + updateBytes(b); + } + + public void updateBigInt(BigInteger b) + { + updateByteString(b.toByteArray()); + } + + public void reset() + { + md.reset(); + } + + public int getDigestLength() + { + return md.getDigestLength(); + } + + public byte[] getDigest() + { + byte[] tmp = new byte[md.getDigestLength()]; + getDigest(tmp); + return tmp; + } + + public void getDigest(byte[] out) + { + getDigest(out, 0); + } + + public void getDigest(byte[] out, int off) + { + try { + md.digest(out, off, out.length - off); + } catch (DigestException e) { + // TODO is this right?! + throw new RuntimeException("Unable to digest", e); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java new file mode 100644 index 0000000000..ea7fcecfd4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java @@ -0,0 +1,12 @@ +package com.trilead.ssh2.crypto.digest; + +/** + * Created by kenny on 2/12/17. + */ +public interface MAC { + void initMac(int seq); + void update(byte[] packetdata, int off, int len); + void getMac(byte[] out, int off); + int size(); + boolean isEncryptThenMac(); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MACs.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MACs.java new file mode 100644 index 0000000000..1238761aa4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/digest/MACs.java @@ -0,0 +1,50 @@ + +package com.trilead.ssh2.crypto.digest; + +/** + * MAC. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: MAC.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public final class MACs +{ + /* Higher Priority First */ + private static final String[] MAC_LIST = { + HMAC.HMAC_SHA2_256_ETM, + HMAC.HMAC_SHA2_512_ETM, + HMAC.HMAC_SHA1_ETM, + HMAC.HMAC_SHA2_256, + HMAC.HMAC_SHA2_512, + HMAC.HMAC_SHA1, + }; + + public final static String[] getMacList() + { + return MAC_LIST; + } + + public final static void checkMacList(String[] macs) + { + for (int i = 0; i < macs.length; i++) { + getKeyLen(macs[i]); + } + } + + public final static int getKeyLen(String type) + { + if (type == null) + throw new IllegalArgumentException("type == null"); + + if (type.startsWith(HMAC.HMAC_SHA1)) + return 20; + if (type.startsWith(HMAC.HMAC_MD5)) + return 16; + if (type.startsWith(HMAC.HMAC_SHA2_256)) + return 32; + if (type.startsWith(HMAC.HMAC_SHA2_512)) + return 64; + + throw new IllegalArgumentException("Unknown algorithm " + type); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactory.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactory.java new file mode 100644 index 0000000000..585331a7a2 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactory.java @@ -0,0 +1,39 @@ +package com.trilead.ssh2.crypto.keys; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactorySpi; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +public class Ed25519KeyFactory extends KeyFactorySpi { + @Override + protected PublicKey engineGeneratePublic(KeySpec keySpec) throws InvalidKeySpecException { + if (keySpec instanceof X509EncodedKeySpec) { + return new Ed25519PublicKey((X509EncodedKeySpec) keySpec); + } + throw new InvalidKeySpecException("Unrecognized key spec: " + keySpec.getClass()); + } + + @Override + protected PrivateKey engineGeneratePrivate(KeySpec keySpec) throws InvalidKeySpecException { + if (keySpec instanceof PKCS8EncodedKeySpec) { + return new Ed25519PrivateKey((PKCS8EncodedKeySpec) keySpec); + } + throw new InvalidKeySpecException("Unrecognized key spec: " + keySpec.getClass()); + } + + @Override + protected T engineGetKeySpec(Key key, Class keySpec) throws InvalidKeySpecException { + throw new InvalidKeySpecException("not implemented yet " + key + " " + keySpec); + } + + @Override + protected Key engineTranslateKey(Key key) throws InvalidKeyException { + throw new InvalidKeyException("No other EdDSA key providers known"); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyPairGenerator.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyPairGenerator.java new file mode 100644 index 0000000000..6328d2c42b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519KeyPairGenerator.java @@ -0,0 +1,25 @@ +package com.trilead.ssh2.crypto.keys; + +import com.google.crypto.tink.subtle.Ed25519Sign; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGeneratorSpi; +import java.security.SecureRandom; + +public class Ed25519KeyPairGenerator extends KeyPairGeneratorSpi { + @Override + public void initialize(int keySize, SecureRandom secureRandom) { + // ignored. + } + + @Override + public KeyPair generateKeyPair() { + try { + Ed25519Sign.KeyPair kp = Ed25519Sign.KeyPair.newKeyPair(); + return new KeyPair(new Ed25519PublicKey(kp.getPublicKey()), new Ed25519PrivateKey(kp.getPrivateKey())); + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java new file mode 100644 index 0000000000..c4eba83bd4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java @@ -0,0 +1,137 @@ +package com.trilead.ssh2.crypto.keys; + +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; + +import javax.security.auth.DestroyFailedException; + +public class Ed25519PrivateKey implements PrivateKey { + private static final byte[] ED25519_OID = new byte[] {43, 101, 112}; + private static final int KEY_BYTES_LENGTH = 32; + private static final int ENCODED_SIZE = 48; + + private final byte[] seed; + private boolean destroyed; + + public Ed25519PrivateKey(byte[] hash) { + this.seed = hash; + } + + public Ed25519PrivateKey(PKCS8EncodedKeySpec keySpec) throws InvalidKeySpecException { + this.seed = decode(keySpec); + } + + @Override + public int hashCode() { + return Arrays.hashCode(seed); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Ed25519PrivateKey)) { + return false; + } + + Ed25519PrivateKey other = (Ed25519PrivateKey) o; + + if (seed == null || other.seed == null || seed.length != other.seed.length) { + return false; + } + + int difference = 0; + for (int i = 0; i < seed.length; i++) { + difference |= seed[i] ^ other.seed[i]; + } + return difference == 0; + } + + @Override + public String getAlgorithm() { + return "EdDSA"; + } + + @Override + public String getFormat() { + return "PKCS#8"; + } + + public byte[] getSeed() { + return seed; + } + + @Override + public byte[] getEncoded() { + // From RFC 8410 section 7 "Private Key Format" + TypesWriter tw = new TypesWriter(); + // ASN.1 Sequence + tw.writeByte(0x30); + tw.writeByte(11 + ED25519_OID.length + seed.length); // Length + // Key version type + tw.writeByte(0x02); // ASN.1 Integer + tw.writeByte(1); // Length + tw.writeByte(0); // v1 == RFC 5208 format + // Algorithm OID - ASN.1 Sequence + tw.writeByte(0x30); + tw.writeByte(ED25519_OID.length + 2); // OID + tw.writeByte(0x06); // ASN.1 OID type + tw.writeByte(ED25519_OID.length); + tw.writeBytes(ED25519_OID); + // Private key sequence + tw.writeByte(0x04); // ASN.1 Octet string + tw.writeByte(2 + seed.length); + tw.writeByte(0x04); // ASN.1 Octet string + tw.writeByte(seed.length); + tw.writeBytes(seed); + + return tw.getBytes(); + } + + private static byte[] decode(PKCS8EncodedKeySpec keySpec) throws InvalidKeySpecException { + byte[] encoded = keySpec.getEncoded(); + if (encoded.length != ENCODED_SIZE) { + throw new InvalidKeySpecException("Key spec is of invalid size"); + } + try { + TypesReader tr = new TypesReader(keySpec.getEncoded()); + if (tr.readByte() != 0x30 || // ASN.1 sequence + tr.readByte() != ENCODED_SIZE - 2 || // Expected size + tr.readByte() != 0x02 || // ASN.1 Integer + tr.readByte() != 1 || // length + tr.readByte() != 0 || // v1 + tr.readByte() != 0x30 || // ASN.1 Sequence + tr.readByte() != ED25519_OID.length + 2 || // OID length + tr.readByte() != 0x06 || // ASN.1 OID + tr.readByte() != ED25519_OID.length) { + throw new InvalidKeySpecException("Key was not encoded correctly"); + } + byte[] oid = tr.readBytes(ED25519_OID.length); + if (!Arrays.equals(ED25519_OID, oid) || + tr.readByte() != 0x04 || // ASN.1 octet string + tr.readByte() != KEY_BYTES_LENGTH + 2 || // length + tr.readByte() != 0x04 || // ASN.1 octet string + tr.readByte() != KEY_BYTES_LENGTH) { + throw new InvalidKeySpecException("Key was not encoded correctly"); + } + return tr.readBytes(KEY_BYTES_LENGTH); + } catch (IOException e) { + throw new InvalidKeySpecException("Key was not encoded correctly", e); + } + } + + @Override + public void destroy() throws DestroyFailedException { + Arrays.fill(seed, (byte) 0); + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519Provider.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519Provider.java new file mode 100644 index 0000000000..369ca72b0a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519Provider.java @@ -0,0 +1,42 @@ +package com.trilead.ssh2.crypto.keys; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.Provider; +import java.security.Security; + +public class Ed25519Provider extends Provider { + public static final String KEY_ALGORITHM = "Ed25519"; + private static final Object sInitLock = new Object(); + private static boolean sInitialized = false; + + public Ed25519Provider() { + super("ConnectBot Ed25519 Provider", 1.0, "Not for use elsewhere"); + AccessController.doPrivileged((PrivilegedAction) () -> { + setup(); + return null; + }); + } + + protected void setup() { + put("KeyFactory." + KEY_ALGORITHM, getClass().getPackage().getName() + ".Ed25519KeyFactory"); + put("KeyPairGenerator." + KEY_ALGORITHM, getClass().getPackage().getName() + ".Ed25519KeyPairGenerator"); + + // id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 } + put("Alg.Alias.KeyFactory.1.3.101.112", KEY_ALGORITHM); + put("Alg.Alias.KeyFactory.EdDSA", KEY_ALGORITHM); + put("Alg.Alias.KeyFactory.OID.1.3.101.112", KEY_ALGORITHM); + put("Alg.Alias.KeyPairGenerator.1.3.101.112", KEY_ALGORITHM); + put("Alg.Alias.KeyPairGenerator.EdDSA", KEY_ALGORITHM); + put("Alg.Alias.KeyPairGenerator.OID.1.3.101.112", KEY_ALGORITHM); + } + + public static void insertIfNeeded() { + synchronized (sInitLock) { + if (!sInitialized) { + Security.addProvider(new Ed25519Provider()); + sInitialized = true; + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java new file mode 100644 index 0000000000..824c925de6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java @@ -0,0 +1,106 @@ +package com.trilead.ssh2.crypto.keys; + +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + +import java.io.IOException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +public class Ed25519PublicKey implements PublicKey { + private static final byte[] ED25519_OID = new byte[]{43, 101, 112}; + private static final int KEY_BYTES_LENGTH = 32; + private static final int ENCODED_SIZE = 44; + + private final byte[] keyBytes; + + public Ed25519PublicKey(byte[] keyBytes) { + this.keyBytes = keyBytes; + } + + public Ed25519PublicKey(X509EncodedKeySpec keySpec) throws InvalidKeySpecException { + keyBytes = decode(keySpec.getEncoded()); + } + + @Override + public String getAlgorithm() { + return "EdDSA"; + } + + @Override + public String getFormat() { + return "X.509"; + } + + @Override + public byte[] getEncoded() { + TypesWriter tw = new TypesWriter(); + tw.writeByte(0x30); // ASN.1 sequence + tw.writeByte(7 + ED25519_OID.length + keyBytes.length); + // Algorithm identifier + tw.writeByte(0x30); // ASN.1 sequence + tw.writeByte(2 + ED25519_OID.length); + tw.writeByte(0x06); // ASN.1 OID + tw.writeByte(ED25519_OID.length); + tw.writeBytes(ED25519_OID); + // Public key + tw.writeByte(0x03); // ASN.1 bit string + tw.writeByte(keyBytes.length + 1); + tw.writeByte(0); + tw.writeBytes(keyBytes); + return tw.getBytes(); + } + + @Override + public int hashCode() { + return Arrays.hashCode(keyBytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Ed25519PublicKey)) { + return false; + } + + Ed25519PublicKey other = (Ed25519PublicKey) o; + if (keyBytes == null || other.keyBytes == null) { + return false; + } + + return Arrays.equals(keyBytes, other.keyBytes); + } + + private static byte[] decode(byte[] input) throws InvalidKeySpecException { + if (input.length != ENCODED_SIZE) { + throw new InvalidKeySpecException("Key is not of correct size"); + } + + try { + TypesReader tr = new TypesReader(input); + if (tr.readByte() != 0x30 || + tr.readByte() != 7 + ED25519_OID.length + KEY_BYTES_LENGTH || + tr.readByte() != 0x30 || + tr.readByte() != 2 + ED25519_OID.length || + tr.readByte() != 0x06 || + tr.readByte() != ED25519_OID.length) { + throw new InvalidKeySpecException("Key was not encoded correctly"); + } + byte[] oid = tr.readBytes(ED25519_OID.length); + if (!Arrays.equals(oid, ED25519_OID) || + tr.readByte() != 0x03 || + tr.readByte() != KEY_BYTES_LENGTH + 1 || + tr.readByte() != 0) { + throw new InvalidKeySpecException("Key was not encoded correctly"); + } + return tr.readBytes(KEY_BYTES_LENGTH); + } catch (IOException e) { + throw new InvalidKeySpecException("Key was not encoded correctly"); + } + } + + public byte[] getAbyte() { + return keyBytes; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/log/Logger.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/log/Logger.java new file mode 100644 index 0000000000..baa225e513 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/log/Logger.java @@ -0,0 +1,54 @@ + +package com.trilead.ssh2.log; + +import com.trilead.ssh2.DebugLogger; + +/** + * Logger - a very simple logger, mainly used during development. + * Is not based on log4j (to reduce external dependencies). + * However, if needed, something like log4j could easily be + * hooked in. + *

+ * For speed reasons, the static variables are not protected + * with semaphores. In other words, if you dynamicaly change the + * logging settings, then some threads may still use the old setting. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Logger.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $ + */ + +public class Logger +{ + public static boolean enabled = false; + public static DebugLogger logger = null; + + private String className; + + public final static Logger getLogger(Class x) + { + return new Logger(x); + } + + public Logger(Class x) + { + this.className = x.getName(); + } + + public final boolean isEnabled() + { + return enabled; + } + + public final void log(int level, String message) + { + if (!enabled) + return; + + DebugLogger target = logger; + + if (target == null) + return; + + target.log(level, className, message); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java new file mode 100644 index 0000000000..95fa3960be --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.packets; + +/** + * PacketGlobalAuthAgent. + * + * @author Kenny Root, kenny@the-b.org + * @version $Id$ + */ +public class PacketChannelAuthAgentReq +{ + byte[] payload; + + public int recipientChannelID; + + public PacketChannelAuthAgentReq(int recipientChannelID) + { + this.recipientChannelID = recipientChannelID; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("auth-agent-req@openssh.com"); + tw.writeBoolean(true); // want reply + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java new file mode 100644 index 0000000000..025a1aeaea --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java @@ -0,0 +1,66 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketChannelOpenConfirmation. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelOpenConfirmation.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketChannelOpenConfirmation +{ + byte[] payload; + + public int recipientChannelID; + public int senderChannelID; + public int initialWindowSize; + public int maxPacketSize; + + public PacketChannelOpenConfirmation(int recipientChannelID, int senderChannelID, int initialWindowSize, + int maxPacketSize) + { + this.recipientChannelID = recipientChannelID; + this.senderChannelID = senderChannelID; + this.initialWindowSize = initialWindowSize; + this.maxPacketSize = maxPacketSize; + } + + public PacketChannelOpenConfirmation(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION) + throw new IOException( + "This is not a SSH_MSG_CHANNEL_OPEN_CONFIRMATION! (" + + packet_type + ")"); + + recipientChannelID = tr.readUINT32(); + senderChannelID = tr.readUINT32(); + initialWindowSize = tr.readUINT32(); + maxPacketSize = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_OPEN_CONFIRMATION packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION); + tw.writeUINT32(recipientChannelID); + tw.writeUINT32(senderChannelID); + tw.writeUINT32(initialWindowSize); + tw.writeUINT32(maxPacketSize); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java new file mode 100644 index 0000000000..ef3430faa4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java @@ -0,0 +1,66 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketChannelOpenFailure. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelOpenFailure.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketChannelOpenFailure +{ + byte[] payload; + + public int recipientChannelID; + public int reasonCode; + public String description; + public String languageTag; + + public PacketChannelOpenFailure(int recipientChannelID, int reasonCode, String description, + String languageTag) + { + this.recipientChannelID = recipientChannelID; + this.reasonCode = reasonCode; + this.description = description; + this.languageTag = languageTag; + } + + public PacketChannelOpenFailure(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_OPEN_FAILURE) + throw new IOException( + "This is not a SSH_MSG_CHANNEL_OPEN_FAILURE! (" + + packet_type + ")"); + + recipientChannelID = tr.readUINT32(); + reasonCode = tr.readUINT32(); + description = tr.readString(); + languageTag = tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_OPEN_FAILURE packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN_FAILURE); + tw.writeUINT32(recipientChannelID); + tw.writeUINT32(reasonCode); + tw.writeString(description); + tw.writeString(languageTag); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java new file mode 100644 index 0000000000..f18f42f2aa --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java @@ -0,0 +1,35 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketChannelTrileadPing. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelTrileadPing.java,v 1.1 2008/03/03 07:01:36 + * cplattne Exp $ + */ +public class PacketChannelTrileadPing +{ + byte[] payload; + + public int recipientChannelID; + + public PacketChannelTrileadPing(int recipientChannelID) + { + this.recipientChannelID = recipientChannelID; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("trilead-ping"); + tw.writeBoolean(true); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java new file mode 100644 index 0000000000..312a1d672a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java @@ -0,0 +1,57 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketChannelWindowAdjust. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelWindowAdjust.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketChannelWindowAdjust +{ + byte[] payload; + + public int recipientChannelID; + public int windowChange; + + public PacketChannelWindowAdjust(int recipientChannelID, int windowChange) + { + this.recipientChannelID = recipientChannelID; + this.windowChange = windowChange; + } + + public PacketChannelWindowAdjust(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST) + throw new IOException( + "This is not a SSH_MSG_CHANNEL_WINDOW_ADJUST! (" + + packet_type + ")"); + + recipientChannelID = tr.readUINT32(); + windowChange = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_WINDOW_ADJUST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST); + tw.writeUINT32(recipientChannelID); + tw.writeUINT32(windowChange); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java new file mode 100644 index 0000000000..de8d91999b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java @@ -0,0 +1,57 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketDisconnect. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketDisconnect.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class PacketDisconnect +{ + byte[] payload; + + int reason; + String desc; + String lang; + + public PacketDisconnect(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_DISCONNECT) + throw new IOException("This is not a Disconnect Packet! (" + packet_type + ")"); + + reason = tr.readUINT32(); + desc = tr.readString(); + lang = tr.readString(); + } + + public PacketDisconnect(int reason, String desc, String lang) + { + this.reason = reason; + this.desc = desc; + this.lang = lang; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_DISCONNECT); + tw.writeUINT32(reason); + tw.writeString(desc); + tw.writeString(lang); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketExtInfo.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketExtInfo.java new file mode 100644 index 0000000000..0639d12000 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketExtInfo.java @@ -0,0 +1,76 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Packet format described here: + * https://tools.ietf.org/html/draft-ietf-curdle-ssh-ext-info-15#section-2.3 + */ +public class PacketExtInfo +{ + private byte[] payload; + + private final Map extNameToValue; + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_EXT_INFO); + tw.writeUINT32(extNameToValue.size()); + for (Entry nameAndValue : extNameToValue.entrySet()) + { + tw.writeString(nameAndValue.getKey()); + tw.writeString(nameAndValue.getValue()); + } + payload = tw.getBytes(); + } + return payload; + } + + public Map getExtNameToValue() + { + return extNameToValue; + } + + public PacketExtInfo(byte[] payload, int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + int packet_type = tr.readByte(); + if (packet_type != Packets.SSH_MSG_EXT_INFO) + { + throw new IOException("This is not a SSH_MSG_EXT_INFO! (" + + packet_type + ")"); + } + + // Type has dynamic number of fields + // First int tells us how many pairs to expect + int numExtensions = tr.readUINT32(); + Map extNameToValue_ = new HashMap<>(numExtensions); + for (int i = 0; i < numExtensions; i++) + { + String name = tr.readString(); + String value = tr.readString(); + extNameToValue_.put(name, value); + } + extNameToValue = Collections.unmodifiableMap(extNameToValue_); + + if (tr.remain() != 0) + { + throw new IOException("Padding in SSH_MSG_EXT_INFO packet!"); + } + } + + public PacketExtInfo(Map extNameToValue) + { + this.extNameToValue = Collections.unmodifiableMap(new HashMap<>(extNameToValue)); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java new file mode 100644 index 0000000000..a5f34086b1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java @@ -0,0 +1,42 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketGlobalCancelForwardRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketGlobalCancelForwardRequest.java,v 1.1 2007/10/15 12:49:55 + * cplattne Exp $ + */ +public class PacketGlobalCancelForwardRequest +{ + byte[] payload; + + public boolean wantReply; + public String bindAddress; + public int bindPort; + + public PacketGlobalCancelForwardRequest(boolean wantReply, String bindAddress, int bindPort) + { + this.wantReply = wantReply; + this.bindAddress = bindAddress; + this.bindPort = bindPort; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_GLOBAL_REQUEST); + + tw.writeString("cancel-tcpip-forward"); + tw.writeBoolean(wantReply); + tw.writeString(bindAddress); + tw.writeUINT32(bindPort); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java new file mode 100644 index 0000000000..23b8ec7654 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java @@ -0,0 +1,41 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketGlobalForwardRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketGlobalForwardRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketGlobalForwardRequest +{ + byte[] payload; + + public boolean wantReply; + public String bindAddress; + public int bindPort; + + public PacketGlobalForwardRequest(boolean wantReply, String bindAddress, int bindPort) + { + this.wantReply = wantReply; + this.bindAddress = bindAddress; + this.bindPort = bindPort; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_GLOBAL_REQUEST); + + tw.writeString("tcpip-forward"); + tw.writeBoolean(wantReply); + tw.writeString(bindAddress); + tw.writeUINT32(bindPort); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java new file mode 100644 index 0000000000..bff20e556e --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java @@ -0,0 +1,32 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketGlobalTrileadPing. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketGlobalTrileadPing.java,v 1.1 2008/03/03 07:01:36 cplattne Exp $ + */ +public class PacketGlobalTrileadPing +{ + byte[] payload; + + public PacketGlobalTrileadPing() + { + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_GLOBAL_REQUEST); + + tw.writeString("trilead-ping"); + tw.writeBoolean(true); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java new file mode 100644 index 0000000000..079e7cf3d4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java @@ -0,0 +1,59 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketIgnore. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketIgnore.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketIgnore +{ + byte[] payload; + + byte[] data; + + public void setData(byte[] data) + { + this.data = data; + payload = null; + } + + public PacketIgnore() + { + } + + public PacketIgnore(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_IGNORE) + throw new IOException("This is not a SSH_MSG_IGNORE packet! (" + packet_type + ")"); + + /* Could parse String body */ + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_IGNORE); + + if (data != null) + tw.writeString(data, 0, data.length); + else + tw.writeString(""); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java new file mode 100644 index 0000000000..77dfdcb6ed --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java @@ -0,0 +1,31 @@ +package com.trilead.ssh2.packets; + +/** + * PacketKexDHInit. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDHInit.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDHInit +{ + byte[] payload; + + byte[] publicKey; + + public PacketKexDHInit(byte[] publicKey) + { + this.publicKey = publicKey; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEXDH_INIT); + tw.writeString(publicKey, 0, publicKey.length); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java new file mode 100644 index 0000000000..2c4b20081d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java @@ -0,0 +1,53 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketKexDHReply. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDHReply.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDHReply +{ + byte[] payload; + + byte[] hostKey; + byte[] publicKey; + byte[] signature; + + public PacketKexDHReply(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEXDH_REPLY) + throw new IOException("This is not a SSH_MSG_KEXDH_REPLY! (" + + packet_type + ")"); + + hostKey = tr.readByteString(); + publicKey = tr.readByteString(); + signature = tr.readByteString(); + + if (tr.remain() != 0) throw new IOException("PADDING IN SSH_MSG_KEXDH_REPLY!"); + } + + public byte[] getF() + { + return publicKey; + } + + public byte[] getHostKey() + { + return hostKey; + } + + public byte[] getSignature() + { + return signature; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java new file mode 100644 index 0000000000..4f9483c2fc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java @@ -0,0 +1,50 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +import java.math.BigInteger; + +/** + * PacketKexDhGexGroup. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexGroup.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexGroup +{ + byte[] payload; + + BigInteger p; + BigInteger g; + + public PacketKexDhGexGroup(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEX_DH_GEX_GROUP) + throw new IllegalArgumentException( + "This is not a SSH_MSG_KEX_DH_GEX_GROUP! (" + packet_type + + ")"); + + p = tr.readMPINT(); + g = tr.readMPINT(); + + if (tr.remain() != 0) + throw new IOException("PADDING IN SSH_MSG_KEX_DH_GEX_GROUP!"); + } + + public BigInteger getG() + { + return g; + } + + public BigInteger getP() + { + return p; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java new file mode 100644 index 0000000000..eb8361bc8a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.packets; + +import java.math.BigInteger; + +/** + * PacketKexDhGexInit. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexInit.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexInit +{ + byte[] payload; + + BigInteger e; + + public PacketKexDhGexInit(BigInteger e) + { + this.e = e; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEX_DH_GEX_INIT); + tw.writeMPInt(e); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java new file mode 100644 index 0000000000..19dd92d2d9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java @@ -0,0 +1,56 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +import java.math.BigInteger; + +/** + * PacketKexDhGexReply. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexReply.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexReply +{ + byte[] payload; + + byte[] hostKey; + BigInteger f; + byte[] signature; + + public PacketKexDhGexReply(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEX_DH_GEX_REPLY) + throw new IOException("This is not a SSH_MSG_KEX_DH_GEX_REPLY! (" + packet_type + ")"); + + hostKey = tr.readByteString(); + f = tr.readMPINT(); + signature = tr.readByteString(); + + if (tr.remain() != 0) + throw new IOException("PADDING IN SSH_MSG_KEX_DH_GEX_REPLY!"); + } + + public BigInteger getF() + { + return f; + } + + public byte[] getHostKey() + { + return hostKey; + } + + public byte[] getSignature() + { + return signature; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java new file mode 100644 index 0000000000..753fd0b9b3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java @@ -0,0 +1,39 @@ +package com.trilead.ssh2.packets; + +import com.trilead.ssh2.DHGexParameters; + +/** + * PacketKexDhGexRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexRequest +{ + byte[] payload; + + int min; + int n; + int max; + + public PacketKexDhGexRequest(DHGexParameters para) + { + this.min = para.getMin_group_len(); + this.n = para.getPref_group_len(); + this.max = para.getMax_group_len(); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEX_DH_GEX_REQUEST); + tw.writeUINT32(min); + tw.writeUINT32(n); + tw.writeUINT32(max); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java new file mode 100644 index 0000000000..8056ad39fc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java @@ -0,0 +1,34 @@ + +package com.trilead.ssh2.packets; + +import com.trilead.ssh2.DHGexParameters; + +/** + * PacketKexDhGexRequestOld. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexRequestOld.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexRequestOld +{ + byte[] payload; + + int n; + + public PacketKexDhGexRequestOld(DHGexParameters para) + { + this.n = para.getPref_group_len(); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEX_DH_GEX_REQUEST_OLD); + tw.writeUINT32(n); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java new file mode 100644 index 0000000000..7805a80ca8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java @@ -0,0 +1,165 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.security.SecureRandom; + +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.transport.KexParameters; + + +/** + * PacketKexInit. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexInit.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexInit +{ + byte[] payload; + + KexParameters kp = new KexParameters(); + + public PacketKexInit(CryptoWishList cwl) + { + kp.cookie = new byte[16]; + new SecureRandom().nextBytes(kp.cookie); + + kp.kex_algorithms = cwl.kexAlgorithms; + kp.server_host_key_algorithms = cwl.serverHostKeyAlgorithms; + kp.encryption_algorithms_client_to_server = cwl.c2s_enc_algos; + kp.encryption_algorithms_server_to_client = cwl.s2c_enc_algos; + kp.mac_algorithms_client_to_server = cwl.c2s_mac_algos; + kp.mac_algorithms_server_to_client = cwl.s2c_mac_algos; + kp.compression_algorithms_client_to_server = cwl.c2s_comp_algos; + kp.compression_algorithms_server_to_client = cwl.s2c_comp_algos; + kp.languages_client_to_server = new String[] {}; + kp.languages_server_to_client = new String[] {}; + kp.first_kex_packet_follows = false; + kp.reserved_field1 = 0; + } + + public PacketKexInit(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEXINIT) + throw new IOException("This is not a KexInitPacket! (" + packet_type + ")"); + + kp.cookie = tr.readBytes(16); + kp.kex_algorithms = tr.readNameList(); + kp.server_host_key_algorithms = tr.readNameList(); + kp.encryption_algorithms_client_to_server = tr.readNameList(); + kp.encryption_algorithms_server_to_client = tr.readNameList(); + kp.mac_algorithms_client_to_server = tr.readNameList(); + kp.mac_algorithms_server_to_client = tr.readNameList(); + kp.compression_algorithms_client_to_server = tr.readNameList(); + kp.compression_algorithms_server_to_client = tr.readNameList(); + kp.languages_client_to_server = tr.readNameList(); + kp.languages_server_to_client = tr.readNameList(); + kp.first_kex_packet_follows = tr.readBoolean(); + kp.reserved_field1 = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in KexInitPacket!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEXINIT); + tw.writeBytes(kp.cookie, 0, 16); + tw.writeNameList(kp.kex_algorithms); + tw.writeNameList(kp.server_host_key_algorithms); + tw.writeNameList(kp.encryption_algorithms_client_to_server); + tw.writeNameList(kp.encryption_algorithms_server_to_client); + tw.writeNameList(kp.mac_algorithms_client_to_server); + tw.writeNameList(kp.mac_algorithms_server_to_client); + tw.writeNameList(kp.compression_algorithms_client_to_server); + tw.writeNameList(kp.compression_algorithms_server_to_client); + tw.writeNameList(kp.languages_client_to_server); + tw.writeNameList(kp.languages_server_to_client); + tw.writeBoolean(kp.first_kex_packet_follows); + tw.writeUINT32(kp.reserved_field1); + payload = tw.getBytes(); + } + return payload; + } + + public KexParameters getKexParameters() + { + return kp; + } + + public String[] getCompression_algorithms_client_to_server() + { + return kp.compression_algorithms_client_to_server; + } + + public String[] getCompression_algorithms_server_to_client() + { + return kp.compression_algorithms_server_to_client; + } + + public byte[] getCookie() + { + return kp.cookie; + } + + public String[] getEncryption_algorithms_client_to_server() + { + return kp.encryption_algorithms_client_to_server; + } + + public String[] getEncryption_algorithms_server_to_client() + { + return kp.encryption_algorithms_server_to_client; + } + + public boolean isFirst_kex_packet_follows() + { + return kp.first_kex_packet_follows; + } + + public String[] getKex_algorithms() + { + return kp.kex_algorithms; + } + + public String[] getLanguages_client_to_server() + { + return kp.languages_client_to_server; + } + + public String[] getLanguages_server_to_client() + { + return kp.languages_server_to_client; + } + + public String[] getMac_algorithms_client_to_server() + { + return kp.mac_algorithms_client_to_server; + } + + public String[] getMac_algorithms_server_to_client() + { + return kp.mac_algorithms_server_to_client; + } + + public int getReserved_field1() + { + return kp.reserved_field1; + } + + public String[] getServer_host_key_algorithms() + { + return kp.server_host_key_algorithms; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java new file mode 100644 index 0000000000..4a4c09a4ce --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java @@ -0,0 +1,46 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketNewKeys. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketNewKeys.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketNewKeys +{ + byte[] payload; + + public PacketNewKeys() + { + } + + public PacketNewKeys(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_NEWKEYS) + throw new IOException("This is not a SSH_MSG_NEWKEYS! (" + + packet_type + ")"); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_NEWKEYS packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_NEWKEYS); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java new file mode 100644 index 0000000000..62d7a1792b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java @@ -0,0 +1,56 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketOpenDirectTCPIPChannel. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketOpenDirectTCPIPChannel.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketOpenDirectTCPIPChannel +{ + byte[] payload; + + int channelID; + int initialWindowSize; + int maxPacketSize; + + String host_to_connect; + int port_to_connect; + String originator_IP_address; + int originator_port; + + public PacketOpenDirectTCPIPChannel(int channelID, int initialWindowSize, int maxPacketSize, + String host_to_connect, int port_to_connect, String originator_IP_address, + int originator_port) + { + this.channelID = channelID; + this.initialWindowSize = initialWindowSize; + this.maxPacketSize = maxPacketSize; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + this.originator_IP_address = originator_IP_address; + this.originator_port = originator_port; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN); + tw.writeString("direct-tcpip"); + tw.writeUINT32(channelID); + tw.writeUINT32(initialWindowSize); + tw.writeUINT32(maxPacketSize); + tw.writeString(host_to_connect); + tw.writeUINT32(port_to_connect); + tw.writeString(originator_IP_address); + tw.writeUINT32(originator_port); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java new file mode 100644 index 0000000000..40dd4abb88 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java @@ -0,0 +1,62 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketOpenSessionChannel. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketOpenSessionChannel.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketOpenSessionChannel +{ + byte[] payload; + + int channelID; + int initialWindowSize; + int maxPacketSize; + + public PacketOpenSessionChannel(int channelID, int initialWindowSize, + int maxPacketSize) + { + this.channelID = channelID; + this.initialWindowSize = initialWindowSize; + this.maxPacketSize = maxPacketSize; + } + + public PacketOpenSessionChannel(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_OPEN) + throw new IOException("This is not a SSH_MSG_CHANNEL_OPEN! (" + + packet_type + ")"); + + channelID = tr.readUINT32(); + initialWindowSize = tr.readUINT32(); + maxPacketSize = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_OPEN packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN); + tw.writeString("session"); + tw.writeUINT32(channelID); + tw.writeUINT32(initialWindowSize); + tw.writeUINT32(maxPacketSize); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java new file mode 100644 index 0000000000..f668910afb --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java @@ -0,0 +1,61 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketServiceAccept. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketServiceAccept.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class PacketServiceAccept +{ + byte[] payload; + + String serviceName; + + public PacketServiceAccept(String serviceName) + { + this.serviceName = serviceName; + } + + public PacketServiceAccept(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_SERVICE_ACCEPT) + throw new IOException("This is not a SSH_MSG_SERVICE_ACCEPT! (" + packet_type + ")"); + + /* Be clever in case the server is not. Some servers seem to violate RFC4253 */ + + if (tr.remain() > 0) + { + serviceName = tr.readString(); + } + else + { + serviceName = ""; + } + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_SERVICE_ACCEPT packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_SERVICE_ACCEPT); + tw.writeString(serviceName); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java new file mode 100644 index 0000000000..51a10adcdf --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java @@ -0,0 +1,52 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketServiceRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketServiceRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketServiceRequest +{ + byte[] payload; + + String serviceName; + + public PacketServiceRequest(String serviceName) + { + this.serviceName = serviceName; + } + + public PacketServiceRequest(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_SERVICE_REQUEST) + throw new IOException("This is not a SSH_MSG_SERVICE_REQUEST! (" + + packet_type + ")"); + + serviceName = tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_SERVICE_REQUEST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_SERVICE_REQUEST); + tw.writeString(serviceName); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java new file mode 100644 index 0000000000..8b509ea2b2 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java @@ -0,0 +1,39 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketSessionExecCommand. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionExecCommand.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionExecCommand +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String command; + + public PacketSessionExecCommand(int recipientChannelID, boolean wantReply, String command) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.command = command; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("exec"); + tw.writeBoolean(wantReply); + tw.writeString(command); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java new file mode 100644 index 0000000000..764c165b60 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java @@ -0,0 +1,57 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketSessionPtyRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionPtyRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionPtyRequest +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String term; + public int character_width; + public int character_height; + public int pixel_width; + public int pixel_height; + public byte[] terminal_modes; + + public PacketSessionPtyRequest(int recipientChannelID, boolean wantReply, String term, + int character_width, int character_height, int pixel_width, int pixel_height, + byte[] terminal_modes) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.term = term; + this.character_width = character_width; + this.character_height = character_height; + this.pixel_width = pixel_width; + this.pixel_height = pixel_height; + this.terminal_modes = terminal_modes; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("pty-req"); + tw.writeBoolean(wantReply); + tw.writeString(term); + tw.writeUINT32(character_width); + tw.writeUINT32(character_height); + tw.writeUINT32(pixel_width); + tw.writeUINT32(pixel_height); + tw.writeString(terminal_modes, 0, terminal_modes.length); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java new file mode 100644 index 0000000000..1e3b558dcc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java @@ -0,0 +1,40 @@ +package com.trilead.ssh2.packets; + +public class PacketSessionPtyResize { + byte[] payload; + + public int recipientChannelID; + public int width; + public int height; + public int pixelWidth; + public int pixelHeight; + + public PacketSessionPtyResize(int recipientChannelID, int width, int height, int pixelWidth, int pixelHeight) { + this.recipientChannelID = recipientChannelID; + this.width = width; + this.height = height; + this.pixelWidth = pixelWidth; + this.pixelHeight = pixelHeight; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("window-change"); + tw.writeBoolean(false); + tw.writeUINT32(width); + tw.writeUINT32(height); + tw.writeUINT32(pixelWidth); + tw.writeUINT32(pixelHeight); + + payload = tw.getBytes(); + } + return payload; + } +} + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java new file mode 100644 index 0000000000..f77d122c9a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java @@ -0,0 +1,36 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketSessionStartShell. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionStartShell.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionStartShell +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + + public PacketSessionStartShell(int recipientChannelID, boolean wantReply) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("shell"); + tw.writeBoolean(wantReply); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java new file mode 100644 index 0000000000..a3b81086cc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java @@ -0,0 +1,40 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketSessionSubsystemRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionSubsystemRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionSubsystemRequest +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String subsystem; + + public PacketSessionSubsystemRequest(int recipientChannelID, boolean wantReply, String subsystem) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.subsystem = subsystem; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("subsystem"); + tw.writeBoolean(wantReply); + tw.writeString(subsystem); + payload = tw.getBytes(); + tw.getBytes(payload); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java new file mode 100644 index 0000000000..c5bc60131a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java @@ -0,0 +1,53 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketSessionX11Request. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionX11Request.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionX11Request +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + + public boolean singleConnection; + String x11AuthenticationProtocol; + String x11AuthenticationCookie; + int x11ScreenNumber; + + public PacketSessionX11Request(int recipientChannelID, boolean wantReply, boolean singleConnection, + String x11AuthenticationProtocol, String x11AuthenticationCookie, int x11ScreenNumber) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + + this.singleConnection = singleConnection; + this.x11AuthenticationProtocol = x11AuthenticationProtocol; + this.x11AuthenticationCookie = x11AuthenticationCookie; + this.x11ScreenNumber = x11ScreenNumber; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("x11-req"); + tw.writeBoolean(wantReply); + + tw.writeBoolean(singleConnection); + tw.writeString(x11AuthenticationProtocol); + tw.writeString(x11AuthenticationCookie); + tw.writeUINT32(x11ScreenNumber); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java new file mode 100644 index 0000000000..bbd8b50b66 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java @@ -0,0 +1,60 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthBanner. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthBanner.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthBanner +{ + byte[] payload; + + String message; + String language; + + public PacketUserauthBanner(String message, String language) + { + this.message = message; + this.language = language; + } + + public String getBanner() + { + return message; + } + + public PacketUserauthBanner(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_BANNER) + throw new IOException("This is not a SSH_MSG_USERAUTH_BANNER! (" + packet_type + ")"); + + message = tr.readString("UTF-8"); + language = tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_REQUEST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_BANNER); + tw.writeString(message); + tw.writeString(language); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java new file mode 100644 index 0000000000..506df55e60 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java @@ -0,0 +1,53 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthBanner. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthFailure.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthFailure +{ + byte[] payload; + + String[] authThatCanContinue; + boolean partialSuccess; + + public PacketUserauthFailure(String[] authThatCanContinue, boolean partialSuccess) + { + this.authThatCanContinue = authThatCanContinue; + this.partialSuccess = partialSuccess; + } + + public PacketUserauthFailure(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_FAILURE) + throw new IOException("This is not a SSH_MSG_USERAUTH_FAILURE! (" + packet_type + ")"); + + authThatCanContinue = tr.readNameList(); + partialSuccess = tr.readBoolean(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_FAILURE packet!"); + } + + public String[] getAuthThatCanContinue() + { + return authThatCanContinue; + } + + public boolean isPartialSuccess() + { + return partialSuccess; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java new file mode 100644 index 0000000000..d47d63a88f --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java @@ -0,0 +1,84 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthInfoRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthInfoRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthInfoRequest +{ + byte[] payload; + + String name; + String instruction; + String languageTag; + int numPrompts; + + String prompt[]; + boolean echo[]; + + public PacketUserauthInfoRequest(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_INFO_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_INFO_REQUEST! (" + packet_type + ")"); + + name = tr.readString(); + instruction = tr.readString(); + languageTag = tr.readString(); + + numPrompts = tr.readUINT32(); + + prompt = new String[numPrompts]; + echo = new boolean[numPrompts]; + + for (int i = 0; i < numPrompts; i++) + { + prompt[i] = tr.readString(); + echo[i] = tr.readBoolean(); + } + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_INFO_REQUEST packet!"); + } + + public boolean[] getEcho() + { + return echo; + } + + public String getInstruction() + { + return instruction; + } + + public String getLanguageTag() + { + return languageTag; + } + + public String getName() + { + return name; + } + + public int getNumPrompts() + { + return numPrompts; + } + + public String[] getPrompt() + { + return prompt; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java new file mode 100644 index 0000000000..054a3835d7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java @@ -0,0 +1,35 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketUserauthInfoResponse. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthInfoResponse.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthInfoResponse +{ + byte[] payload; + + String[] responses; + + public PacketUserauthInfoResponse(String[] responses) + { + this.responses = responses; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_INFO_RESPONSE); + tw.writeUINT32(responses.length); + for (int i = 0; i < responses.length; i++) + tw.writeString(responses[i]); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java new file mode 100644 index 0000000000..0e4e91f2a7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java @@ -0,0 +1,43 @@ + +package com.trilead.ssh2.packets; + +import java.io.UnsupportedEncodingException; + +/** + * PacketUserauthRequestInteractive. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestInteractive.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestInteractive +{ + byte[] payload; + + String userName; + String serviceName; + String[] submethods; + + public PacketUserauthRequestInteractive(String serviceName, String user, String[] submethods) + { + this.serviceName = serviceName; + this.userName = user; + this.submethods = submethods; + } + + public byte[] getPayload() throws UnsupportedEncodingException { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName, "UTF-8"); + tw.writeString(serviceName); + tw.writeString("keyboard-interactive"); + tw.writeString(""); // draft-ietf-secsh-newmodes-04.txt says that + // the language tag should be empty. + tw.writeNameList(submethods); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java new file mode 100644 index 0000000000..60a3defa00 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java @@ -0,0 +1,61 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * PacketUserauthRequestPassword. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestNone.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestNone +{ + byte[] payload; + + String userName; + String serviceName; + + public PacketUserauthRequestNone(String serviceName, String user) + { + this.serviceName = serviceName; + this.userName = user; + } + + public PacketUserauthRequestNone(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST! (" + packet_type + ")"); + + userName = tr.readString(); + serviceName = tr.readString(); + + String method = tr.readString(); + + if (!method.equals("none")) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST with type none!"); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_REQUEST packet!"); + } + + public byte[] getPayload() throws UnsupportedEncodingException { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName, "UTF-8"); + tw.writeString(serviceName); + tw.writeString("none"); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java new file mode 100644 index 0000000000..801d1b7e1b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java @@ -0,0 +1,67 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * PacketUserauthRequestPassword. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestPassword.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestPassword +{ + byte[] payload; + + String userName; + String serviceName; + String password; + + public PacketUserauthRequestPassword(String serviceName, String user, String pass) + { + this.serviceName = serviceName; + this.userName = user; + this.password = pass; + } + + public PacketUserauthRequestPassword(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST! (" + packet_type + ")"); + + userName = tr.readString(); + serviceName = tr.readString(); + + String method = tr.readString(); + + if (!method.equals("password")) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST with type password!"); + + /* ... */ + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_REQUEST packet!"); + } + + public byte[] getPayload() throws UnsupportedEncodingException { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName, "UTF-8"); + tw.writeString(serviceName); + tw.writeString("password"); + tw.writeBoolean(false); + tw.writeString(password, "UTF-8"); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java new file mode 100644 index 0000000000..4c6945ea30 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java @@ -0,0 +1,65 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * PacketUserauthRequestPublicKey. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestPublicKey.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestPublicKey +{ + byte[] payload; + + String userName; + String serviceName; + String password; + String pkAlgoName; + byte[] pk; + byte[] sig; + + public PacketUserauthRequestPublicKey(String serviceName, String user, + String pkAlgorithmName, byte[] pk, byte[] sig) + { + this.serviceName = serviceName; + this.userName = user; + this.pkAlgoName = pkAlgorithmName; + this.pk = pk; + this.sig = sig; + } + + public PacketUserauthRequestPublicKey(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST! (" + + packet_type + ")"); + + throw new IOException("Not implemented!"); + } + + public byte[] getPayload() throws UnsupportedEncodingException { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName, "UTF-8"); + tw.writeString(serviceName); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString(pkAlgoName); + tw.writeString(pk, 0, pk.length); + tw.writeString(sig, 0, sig.length); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/Packets.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/Packets.java new file mode 100644 index 0000000000..3d00aa4498 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/Packets.java @@ -0,0 +1,153 @@ + +package com.trilead.ssh2.packets; + +/** + * Packets. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Packets.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class Packets +{ + public static final int SSH_MSG_DISCONNECT = 1; + public static final int SSH_MSG_IGNORE = 2; + public static final int SSH_MSG_UNIMPLEMENTED = 3; + public static final int SSH_MSG_DEBUG = 4; + public static final int SSH_MSG_SERVICE_REQUEST = 5; + public static final int SSH_MSG_SERVICE_ACCEPT = 6; + public static final int SSH_MSG_EXT_INFO = 7; + + public static final int SSH_MSG_KEXINIT = 20; + public static final int SSH_MSG_NEWKEYS = 21; + + public static final int SSH_MSG_KEXDH_INIT = 30; + public static final int SSH_MSG_KEXDH_REPLY = 31; + + public static final int SSH_MSG_KEX_DH_GEX_REQUEST_OLD = 30; + public static final int SSH_MSG_KEX_DH_GEX_REQUEST = 34; + public static final int SSH_MSG_KEX_DH_GEX_GROUP = 31; + public static final int SSH_MSG_KEX_DH_GEX_INIT = 32; + public static final int SSH_MSG_KEX_DH_GEX_REPLY = 33; + + public static final int SSH_MSG_USERAUTH_REQUEST = 50; + public static final int SSH_MSG_USERAUTH_FAILURE = 51; + public static final int SSH_MSG_USERAUTH_SUCCESS = 52; + public static final int SSH_MSG_USERAUTH_BANNER = 53; + public static final int SSH_MSG_USERAUTH_INFO_REQUEST = 60; + public static final int SSH_MSG_USERAUTH_INFO_RESPONSE = 61; + + public static final int SSH_MSG_GLOBAL_REQUEST = 80; + public static final int SSH_MSG_REQUEST_SUCCESS = 81; + public static final int SSH_MSG_REQUEST_FAILURE = 82; + + public static final int SSH_MSG_CHANNEL_OPEN = 90; + public static final int SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91; + public static final int SSH_MSG_CHANNEL_OPEN_FAILURE = 92; + public static final int SSH_MSG_CHANNEL_WINDOW_ADJUST = 93; + public static final int SSH_MSG_CHANNEL_DATA = 94; + public static final int SSH_MSG_CHANNEL_EXTENDED_DATA = 95; + public static final int SSH_MSG_CHANNEL_EOF = 96; + public static final int SSH_MSG_CHANNEL_CLOSE = 97; + public static final int SSH_MSG_CHANNEL_REQUEST = 98; + public static final int SSH_MSG_CHANNEL_SUCCESS = 99; + public static final int SSH_MSG_CHANNEL_FAILURE = 100; + + public static final int SSH_EXTENDED_DATA_STDERR = 1; + + public static final int SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1; + public static final int SSH_DISCONNECT_PROTOCOL_ERROR = 2; + public static final int SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3; + public static final int SSH_DISCONNECT_RESERVED = 4; + public static final int SSH_DISCONNECT_MAC_ERROR = 5; + public static final int SSH_DISCONNECT_COMPRESSION_ERROR = 6; + public static final int SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7; + public static final int SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8; + public static final int SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9; + public static final int SSH_DISCONNECT_CONNECTION_LOST = 10; + public static final int SSH_DISCONNECT_BY_APPLICATION = 11; + public static final int SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12; + public static final int SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13; + public static final int SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14; + public static final int SSH_DISCONNECT_ILLEGAL_USER_NAME = 15; + + public static final int SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1; + public static final int SSH_OPEN_CONNECT_FAILED = 2; + public static final int SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3; + public static final int SSH_OPEN_RESOURCE_SHORTAGE = 4; + + private static final String[] reverseNames = new String[101]; + + private Packets() { } + + static + { + reverseNames[1] = "SSH_MSG_DISCONNECT"; + reverseNames[2] = "SSH_MSG_IGNORE"; + reverseNames[3] = "SSH_MSG_UNIMPLEMENTED"; + reverseNames[4] = "SSH_MSG_DEBUG"; + reverseNames[5] = "SSH_MSG_SERVICE_REQUEST"; + reverseNames[6] = "SSH_MSG_SERVICE_ACCEPT"; + reverseNames[7] = "SSH_MSG_EXT_INFO"; + + reverseNames[20] = "SSH_MSG_KEXINIT"; + reverseNames[21] = "SSH_MSG_NEWKEYS"; + + reverseNames[30] = "SSH_MSG_KEXDH_INIT"; + reverseNames[31] = "SSH_MSG_KEXDH_REPLY/SSH_MSG_KEX_DH_GEX_GROUP"; + reverseNames[32] = "SSH_MSG_KEX_DH_GEX_INIT"; + reverseNames[33] = "SSH_MSG_KEX_DH_GEX_REPLY"; + reverseNames[34] = "SSH_MSG_KEX_DH_GEX_REQUEST"; + + reverseNames[50] = "SSH_MSG_USERAUTH_REQUEST"; + reverseNames[51] = "SSH_MSG_USERAUTH_FAILURE"; + reverseNames[52] = "SSH_MSG_USERAUTH_SUCCESS"; + reverseNames[53] = "SSH_MSG_USERAUTH_BANNER"; + + reverseNames[60] = "SSH_MSG_USERAUTH_INFO_REQUEST"; + reverseNames[61] = "SSH_MSG_USERAUTH_INFO_RESPONSE"; + + reverseNames[80] = "SSH_MSG_GLOBAL_REQUEST"; + reverseNames[81] = "SSH_MSG_REQUEST_SUCCESS"; + reverseNames[82] = "SSH_MSG_REQUEST_FAILURE"; + + reverseNames[90] = "SSH_MSG_CHANNEL_OPEN"; + reverseNames[91] = "SSH_MSG_CHANNEL_OPEN_CONFIRMATION"; + reverseNames[92] = "SSH_MSG_CHANNEL_OPEN_FAILURE"; + reverseNames[93] = "SSH_MSG_CHANNEL_WINDOW_ADJUST"; + reverseNames[94] = "SSH_MSG_CHANNEL_DATA"; + reverseNames[95] = "SSH_MSG_CHANNEL_EXTENDED_DATA"; + reverseNames[96] = "SSH_MSG_CHANNEL_EOF"; + reverseNames[97] = "SSH_MSG_CHANNEL_CLOSE"; + reverseNames[98] = "SSH_MSG_CHANNEL_REQUEST"; + reverseNames[99] = "SSH_MSG_CHANNEL_SUCCESS"; + reverseNames[100] = "SSH_MSG_CHANNEL_FAILURE"; + } + + public static final String getMessageName(int type) + { + String res = null; + + if ((type >= 0) && (type < reverseNames.length)) + { + res = reverseNames[type]; + } + + return (res == null) ? ("UNKNOWN MSG " + type) : res; + } + + // public static final void debug(String tag, byte[] msg) + // { + // System.err.println(tag + " Type: " + msg[0] + ", LEN: " + msg.length); + // + // for (int i = 0; i < msg.length; i++) + // { + // if (((msg[i] >= 'a') && (msg[i] <= 'z')) || ((msg[i] >= 'A') && (msg[i] <= 'Z')) + // || ((msg[i] >= '0') && (msg[i] <= '9')) || (msg[i] == ' ')) + // System.err.print((char) msg[i]); + // else + // System.err.print("."); + // } + // System.err.println(); + // System.err.flush(); + // } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesReader.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesReader.java new file mode 100644 index 0000000000..df54f1f4c5 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesReader.java @@ -0,0 +1,192 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +import com.trilead.ssh2.util.Tokenizer; + + +/** + * TypesReader. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TypesReader.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TypesReader +{ + byte[] arr; + int pos = 0; + int max = 0; + + public TypesReader(byte[] arr) + { + this.arr = arr; + pos = 0; + max = arr.length; + } + + public TypesReader(byte[] arr, int off) + { + this.arr = arr; + this.pos = off; + this.max = arr.length; + + if ((pos < 0) || (pos > arr.length)) + throw new IllegalArgumentException("Illegal offset."); + } + + public TypesReader(byte[] arr, int off, int len) + { + this.arr = arr; + this.pos = off; + this.max = off + len; + + if ((pos < 0) || (pos >= arr.length)) + throw new IllegalArgumentException("Illegal offset."); + + if ((max < 0) || (max > arr.length)) + throw new IllegalArgumentException("Illegal length."); + } + + public int readByte() throws IOException + { + if (pos >= max) + throw new IOException("Packet too short."); + + return (arr[pos++] & 0xff); + } + + public byte[] readBytes(int len) throws IOException + { + if (len < 0) + throw new IOException("Negative length requested"); + + if ((pos + len) > max) + throw new IOException("Packet too short."); + + byte[] res = new byte[len]; + + System.arraycopy(arr, pos, res, 0, len); + pos += len; + + return res; + } + + public void readBytes(byte[] dst, int off, int len) throws IOException + { + if (off < 0 || len < 0) + throw new IOException("Negative offset or length specified"); + + if (len > dst.length - off) + throw new IOException("Length too long for output buffer"); + + if ((pos + len) > max) + throw new IOException("Packet too short."); + + System.arraycopy(arr, pos, dst, off, len); + pos += len; + } + + public boolean readBoolean() throws IOException + { + if (pos >= max) + throw new IOException("Packet too short."); + + return (arr[pos++] != 0); + } + + public int readUINT32() throws IOException + { + if ((pos + 4) > max) + throw new IOException("Packet too short."); + + return ((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) + | (arr[pos++] & 0xff); + } + + public long readUINT64() throws IOException + { + if ((pos + 8) > max) + throw new IOException("Packet too short."); + + long high = ((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) + | (arr[pos++] & 0xff); /* sign extension may take place - will be shifted away =) */ + + long low = ((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) + | (arr[pos++] & 0xff); /* sign extension may take place - handle below */ + + return (high << 32) | (low & 0xffffffffl); /* see Java language spec (15.22.1, 5.6.2) */ + } + + public BigInteger readMPINT() throws IOException + { + BigInteger b; + + byte[] raw = readByteString(); + + if (raw.length == 0) + b = BigInteger.ZERO; + else + b = new BigInteger(raw); + + return b; + } + + public byte[] readByteString() throws IOException + { + int len = readUINT32(); + + if ((len + pos) > max) + throw new IOException("Malformed SSH byte string."); + + byte[] res = new byte[len]; + System.arraycopy(arr, pos, res, 0, len); + pos += len; + return res; + } + + public String readString(String charsetName) throws IOException + { + int len = readUINT32(); + + if ((len + pos) > max) + throw new IOException("Malformed SSH string."); + + String res = (charsetName == null) ? new String(arr, pos, len) : new String(arr, pos, len, charsetName); + pos += len; + + return res; + } + + public String readString() throws IOException + { + int len = readUINT32(); + + if ((len + pos) > max) + throw new IOException("Malformed SSH string."); + + String res; + try { + res = new String(arr, pos, len, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + res = new String(arr, pos, len); + } + + pos += len; + + return res; + } + + public String[] readNameList() throws IOException + { + return Tokenizer.parseTokens(readString(), ','); + } + + public int remain() + { + return max - pos; + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesWriter.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesWriter.java new file mode 100644 index 0000000000..ecd31ab22d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/packets/TypesWriter.java @@ -0,0 +1,166 @@ + +package com.trilead.ssh2.packets; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +/** + * TypesWriter. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TypesWriter.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TypesWriter +{ + byte arr[]; + int pos; + + public TypesWriter() + { + arr = new byte[256]; + pos = 0; + } + + private void resize(int len) + { + byte new_arr[] = new byte[len]; + System.arraycopy(arr, 0, new_arr, 0, arr.length); + arr = new_arr; + } + + public int length() + { + return pos; + } + + public byte[] getBytes() + { + byte[] dst = new byte[pos]; + System.arraycopy(arr, 0, dst, 0, pos); + return dst; + } + + public void getBytes(byte dst[]) + { + System.arraycopy(arr, 0, dst, 0, pos); + } + + public void writeUINT32(int val, int off) + { + if ((off + 4) > arr.length) + resize(off + 32); + + arr[off++] = (byte) (val >> 24); + arr[off++] = (byte) (val >> 16); + arr[off++] = (byte) (val >> 8); + arr[off++] = (byte) val; + } + + public void writeUINT32(int val) + { + writeUINT32(val, pos); + pos += 4; + } + + public void writeUINT64(long val) + { + if ((pos + 8) > arr.length) + resize(arr.length + 32); + + arr[pos++] = (byte) (val >> 56); + arr[pos++] = (byte) (val >> 48); + arr[pos++] = (byte) (val >> 40); + arr[pos++] = (byte) (val >> 32); + arr[pos++] = (byte) (val >> 24); + arr[pos++] = (byte) (val >> 16); + arr[pos++] = (byte) (val >> 8); + arr[pos++] = (byte) val; + } + + public void writeBoolean(boolean v) + { + if ((pos + 1) > arr.length) + resize(arr.length + 32); + + arr[pos++] = v ? (byte) 1 : (byte) 0; + } + + public void writeByte(int v, int off) + { + if ((off + 1) > arr.length) + resize(off + 32); + + arr[off] = (byte) v; + } + + public void writeByte(int v) + { + writeByte(v, pos); + pos++; + } + + public void writeMPInt(BigInteger b) + { + byte raw[] = b.toByteArray(); + + if ((raw.length == 1) && (raw[0] == 0)) + writeUINT32(0); /* String with zero bytes of data */ + else + writeString(raw, 0, raw.length); + } + + public void writeBytes(byte[] buff) + { + writeBytes(buff, 0, buff.length); + } + + public void writeBytes(byte[] buff, int off, int len) + { + if ((pos + len) > arr.length) + resize(arr.length + len + 32); + + System.arraycopy(buff, off, arr, pos, len); + pos += len; + } + + public void writeString(byte[] buff, int off, int len) + { + writeUINT32(len); + writeBytes(buff, off, len); + } + + public void writeString(String v) + { + byte[] b; + + /* All Java JVMs must support ISO-8859-1 */ + try { + b = v.getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + b = v.getBytes(); + } + + writeUINT32(b.length); + writeBytes(b, 0, b.length); + } + + public void writeString(String v, String charsetName) throws UnsupportedEncodingException + { + byte[] b = (charsetName == null) ? v.getBytes() : v.getBytes(charsetName); + + writeUINT32(b.length); + writeBytes(b, 0, b.length); + } + + public void writeNameList(String v[]) + { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < v.length; i++) + { + if (i > 0) + sb.append(','); + sb.append(v[i]); + } + writeString(sb.toString()); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java new file mode 100644 index 0000000000..8c9115e955 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java @@ -0,0 +1,38 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Values for the 'text-hint' field in the SFTP ATTRS data type. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttrTextHints.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttrTextHints +{ + /** + * The server knows the file is a text file, and should be opened + * using the SSH_FXF_ACCESS_TEXT_MODE flag. + */ + public static final int SSH_FILEXFER_ATTR_KNOWN_TEXT = 0x00; + + /** + * The server has applied a heuristic or other mechanism and + * believes that the file should be opened with the + * SSH_FXF_ACCESS_TEXT_MODE flag. + */ + public static final int SSH_FILEXFER_ATTR_GUESSED_TEXT = 0x01; + + /** + * The server knows the file has binary content. + */ + public static final int SSH_FILEXFER_ATTR_KNOWN_BINARY = 0x02; + + /** + * The server has applied a heuristic or other mechanism and + * believes has binary content, and should not be opened with the + * SSH_FXF_ACCESS_TEXT_MODE flag. + */ + public static final int SSH_FILEXFER_ATTR_GUESSED_BINARY = 0x03; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribBits.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribBits.java new file mode 100644 index 0000000000..1a4afd4d7c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribBits.java @@ -0,0 +1,129 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Attribute Bits for the "attrib-bits" and "attrib-bits-valid" fields + * of the SFTP ATTR data type. + *

+ * Yes, these are the "attrib-bits", even though they have "_FLAGS_" in + * their name. Don't ask - I did not invent it. + *

+ * "These fields, taken together, reflect various attributes of the file + * or directory, on the server. Bits not set in 'attrib-bits-valid' MUST be + * ignored in the 'attrib-bits' field. This allows both the server and the + * client to communicate only the bits it knows about without inadvertently + * twiddling bits they don't understand." + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribBits.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribBits +{ + /** + * Advisory, read-only bit. This bit is not part of the access + * control information on the file, but is rather an advisory field + * indicating that the file should not be written. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_READONLY = 0x00000001; + + /** + * The file is part of the operating system. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_SYSTEM = 0x00000002; + + /** + * File SHOULD NOT be shown to user unless specifically requested. + * For example, most UNIX systems SHOULD set this bit if the filename + * begins with a 'period'. This bit may be read-only (see section 5.4 of + * the SFTP standard draft). Most UNIX systems will not allow this to be + * changed. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_HIDDEN = 0x00000004; + + /** + * This attribute applies only to directories. This attribute is + * always read-only, and cannot be modified. This attribute means + * that files and directory names in this directory should be compared + * without regard to case. + *

+ * It is recommended that where possible, the server's filesystem be + * allowed to do comparisons. For example, if a client wished to prompt + * a user before overwriting a file, it should not compare the new name + * with the previously retrieved list of names in the directory. Rather, + * it should first try to create the new file by specifying + * SSH_FXF_CREATE_NEW flag. Then, if this fails and returns + * SSH_FX_FILE_ALREADY_EXISTS, it should prompt the user and then retry + * the create specifying SSH_FXF_CREATE_TRUNCATE. + *

+ * Unless otherwise specified, filenames are assumed to be case sensitive. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE = 0x00000008; + + /** + * The file should be included in backup / archive operations. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_ARCHIVE = 0x00000010; + + /** + * The file is stored on disk using file-system level transparent + * encryption. This flag does not affect the file data on the wire + * (for either READ or WRITE requests.) + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED = 0x00000020; + + /** + * The file is stored on disk using file-system level transparent + * compression. This flag does not affect the file data on the wire. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_COMPRESSED = 0x00000040; + + /** + * The file is a sparse file; this means that file blocks that have + * not been explicitly written are not stored on disk. For example, if + * a client writes a buffer at 10 M from the beginning of the file, + * the blocks between the previous EOF marker and the 10 M offset would + * not consume physical disk space. + *

+ * Some servers may store all files as sparse files, in which case + * this bit will be unconditionally set. Other servers may not have + * a mechanism for determining if the file is sparse, and so the file + * MAY be stored sparse even if this flag is not set. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_SPARSE = 0x00000080; + + /** + * Opening the file without either the SSH_FXF_ACCESS_APPEND_DATA or + * the SSH_FXF_ACCESS_APPEND_DATA_ATOMIC flag (see section 8.1.1.3 + * of the SFTP standard draft) MUST result in an + * SSH_FX_INVALID_PARAMETER error. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY = 0x00000100; + + /** + * The file cannot be deleted or renamed, no hard link can be created + * to this file, and no data can be written to the file. + *

+ * This bit implies a stronger level of protection than + * SSH_FILEXFER_ATTR_FLAGS_READONLY, the file permission mask or ACLs. + * Typically even the superuser cannot write to immutable files, and + * only the superuser can set or remove the bit. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE = 0x00000200; + + /** + * When the file is modified, the changes are written synchronously + * to the disk. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_SYNC = 0x00000400; + + /** + * The server MAY include this bit in a directory listing or realpath + * response. It indicates there was a failure in the translation to UTF-8. + * If this flag is included, the server SHOULD also include the + * UNTRANSLATED_NAME attribute. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_TRANSLATION_ERR = 0x00000800; + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java new file mode 100644 index 0000000000..c364afe665 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java @@ -0,0 +1,112 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Attribute Flags. The 'valid-attribute-flags' field in + * the SFTP ATTRS data type specifies which of the fields are actually present. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribFlags.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribFlags +{ + /** + * Indicates that the 'allocation-size' field is present. + */ + public static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001; + + /** Protocol version 6: + * 0x00000002 was used in a previous version of this protocol. + * It is now a reserved value and MUST NOT appear in the mask. + * Some future version of this protocol may reuse this value. + */ + public static final int SSH_FILEXFER_ATTR_V3_UIDGID = 0x00000002; + + /** + * Indicates that the 'permissions' field is present. + */ + public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004; + + /** + * Indicates that the 'atime' and 'mtime' field are present + * (protocol v3). + */ + public static final int SSH_FILEXFER_ATTR_V3_ACMODTIME = 0x00000008; + + /** + * Indicates that the 'atime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_ACCESSTIME = 0x00000008; + + /** + * Indicates that the 'createtime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_CREATETIME = 0x00000010; + + /** + * Indicates that the 'mtime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_MODIFYTIME = 0x00000020; + + /** + * Indicates that the 'acl' field is present. + */ + public static final int SSH_FILEXFER_ATTR_ACL = 0x00000040; + + /** + * Indicates that the 'owner' and 'group' fields are present. + */ + public static final int SSH_FILEXFER_ATTR_OWNERGROUP = 0x00000080; + + /** + * Indicates that additionally to the 'atime', 'createtime', + * 'mtime' and 'ctime' fields (if present), there is also + * 'atime-nseconds', 'createtime-nseconds', 'mtime-nseconds' + * and 'ctime-nseconds'. + */ + public static final int SSH_FILEXFER_ATTR_SUBSECOND_TIMES = 0x00000100; + + /** + * Indicates that the 'attrib-bits' and 'attrib-bits-valid' + * fields are present. + */ + public static final int SSH_FILEXFER_ATTR_BITS = 0x00000200; + + /** + * Indicates that the 'allocation-size' field is present. + */ + public static final int SSH_FILEXFER_ATTR_ALLOCATION_SIZE = 0x00000400; + + /** + * Indicates that the 'text-hint' field is present. + */ + public static final int SSH_FILEXFER_ATTR_TEXT_HINT = 0x00000800; + + /** + * Indicates that the 'mime-type' field is present. + */ + public static final int SSH_FILEXFER_ATTR_MIME_TYPE = 0x00001000; + + /** + * Indicates that the 'link-count' field is present. + */ + public static final int SSH_FILEXFER_ATTR_LINK_COUNT = 0x00002000; + + /** + * Indicates that the 'untranslated-name' field is present. + */ + public static final int SSH_FILEXFER_ATTR_UNTRANSLATED_NAME = 0x00004000; + + /** + * Indicates that the 'ctime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_CTIME = 0x00008000; + + /** + * Indicates that the 'extended-count' field (and probablby some + * 'extensions') is present. + */ + public static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java new file mode 100644 index 0000000000..5b668bdf72 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java @@ -0,0 +1,32 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Permissions for the 'permissions' field in the SFTP ATTRS data type. + *

+ * "The 'permissions' field contains a bit mask specifying file permissions. + * These permissions correspond to the st_mode field of the stat structure + * defined by POSIX [IEEE.1003-1.1996]." + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribPermissions.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribPermissions +{ + /* Octal values! */ + + public static final int S_IRUSR = 0400; + public static final int S_IWUSR = 0200; + public static final int S_IXUSR = 0100; + public static final int S_IRGRP = 0040; + public static final int S_IWGRP = 0020; + public static final int S_IXGRP = 0010; + public static final int S_IROTH = 0004; + public static final int S_IWOTH = 0002; + public static final int S_IXOTH = 0001; + public static final int S_ISUID = 04000; + public static final int S_ISGID = 02000; + public static final int S_ISVTX = 01000; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java new file mode 100644 index 0000000000..8302fe2b2b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java @@ -0,0 +1,28 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Types for the 'type' field in the SFTP ATTRS data type. + *

+ * "On a POSIX system, these values would be derived from the mode field + * of the stat structure. SPECIAL should be used for files that are of + * a known type which cannot be expressed in the protocol. UNKNOWN + * should be used if the type is not known." + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribTypes.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribTypes +{ + public static final int SSH_FILEXFER_TYPE_REGULAR = 1; + public static final int SSH_FILEXFER_TYPE_DIRECTORY = 2; + public static final int SSH_FILEXFER_TYPE_SYMLINK = 3; + public static final int SSH_FILEXFER_TYPE_SPECIAL = 4; + public static final int SSH_FILEXFER_TYPE_UNKNOWN = 5; + public static final int SSH_FILEXFER_TYPE_SOCKET = 6; + public static final int SSH_FILEXFER_TYPE_CHAR_DEVICE = 7; + public static final int SSH_FILEXFER_TYPE_BLOCK_DEVICE = 8; + public static final int SSH_FILEXFER_TYPE_FIFO = 9; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java new file mode 100644 index 0000000000..7317a0020d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java @@ -0,0 +1,104 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Error Codes + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ErrorCodes.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class ErrorCodes +{ + public static final int SSH_FX_OK = 0; + public static final int SSH_FX_EOF = 1; + public static final int SSH_FX_NO_SUCH_FILE = 2; + public static final int SSH_FX_PERMISSION_DENIED = 3; + public static final int SSH_FX_FAILURE = 4; + public static final int SSH_FX_BAD_MESSAGE = 5; + public static final int SSH_FX_NO_CONNECTION = 6; + public static final int SSH_FX_CONNECTION_LOST = 7; + public static final int SSH_FX_OP_UNSUPPORTED = 8; + public static final int SSH_FX_INVALID_HANDLE = 9; + public static final int SSH_FX_NO_SUCH_PATH = 10; + public static final int SSH_FX_FILE_ALREADY_EXISTS = 11; + public static final int SSH_FX_WRITE_PROTECT = 12; + public static final int SSH_FX_NO_MEDIA = 13; + public static final int SSH_FX_NO_SPACE_ON_FILESYSTEM = 14; + public static final int SSH_FX_QUOTA_EXCEEDED = 15; + public static final int SSH_FX_UNKNOWN_PRINCIPAL = 16; + public static final int SSH_FX_LOCK_CONFLICT = 17; + public static final int SSH_FX_DIR_NOT_EMPTY = 18; + public static final int SSH_FX_NOT_A_DIRECTORY = 19; + public static final int SSH_FX_INVALID_FILENAME = 20; + public static final int SSH_FX_LINK_LOOP = 21; + public static final int SSH_FX_CANNOT_DELETE = 22; + public static final int SSH_FX_INVALID_PARAMETER = 23; + public static final int SSH_FX_FILE_IS_A_DIRECTORY = 24; + public static final int SSH_FX_BYTE_RANGE_LOCK_CONFLICT = 25; + public static final int SSH_FX_BYTE_RANGE_LOCK_REFUSED = 26; + public static final int SSH_FX_DELETE_PENDING = 27; + public static final int SSH_FX_FILE_CORRUPT = 28; + public static final int SSH_FX_OWNER_INVALID = 29; + public static final int SSH_FX_GROUP_INVALID = 30; + public static final int SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK = 31; + + private static final String[][] messages = { + + { "SSH_FX_OK", "Indicates successful completion of the operation." }, + { "SSH_FX_EOF", + "An attempt to read past the end-of-file was made; or, there are no more directory entries to return." }, + { "SSH_FX_NO_SUCH_FILE", "A reference was made to a file which does not exist." }, + { "SSH_FX_PERMISSION_DENIED", "The user does not have sufficient permissions to perform the operation." }, + { "SSH_FX_FAILURE", "An error occurred, but no specific error code exists to describe the failure." }, + { "SSH_FX_BAD_MESSAGE", "A badly formatted packet or other SFTP protocol incompatibility was detected." }, + { "SSH_FX_NO_CONNECTION", "There is no connection to the server." }, + { "SSH_FX_CONNECTION_LOST", "The connection to the server was lost." }, + { "SSH_FX_OP_UNSUPPORTED", + "An attempted operation could not be completed by the server because the server does not support the operation." }, + { "SSH_FX_INVALID_HANDLE", "The handle value was invalid." }, + { "SSH_FX_NO_SUCH_PATH", "The file path does not exist or is invalid." }, + { "SSH_FX_FILE_ALREADY_EXISTS", "The file already exists." }, + { "SSH_FX_WRITE_PROTECT", "The file is on read-only media, or the media is write protected." }, + { "SSH_FX_NO_MEDIA", + "The requested operation cannot be completed because there is no media available in the drive." }, + { "SSH_FX_NO_SPACE_ON_FILESYSTEM", + "The requested operation cannot be completed because there is insufficient free space on the filesystem." }, + { "SSH_FX_QUOTA_EXCEEDED", + "The operation cannot be completed because it would exceed the user's storage quota." }, + { + "SSH_FX_UNKNOWN_PRINCIPAL", + "A principal referenced by the request (either the 'owner', 'group', or 'who' field of an ACL), was unknown. The error specific data contains the problematic names." }, + { "SSH_FX_LOCK_CONFLICT", "The file could not be opened because it is locked by another process." }, + { "SSH_FX_DIR_NOT_EMPTY", "The directory is not empty." }, + { "SSH_FX_NOT_A_DIRECTORY", "The specified file is not a directory." }, + { "SSH_FX_INVALID_FILENAME", "The filename is not valid." }, + { "SSH_FX_LINK_LOOP", + "Too many symbolic links encountered or, an SSH_FXF_NOFOLLOW open encountered a symbolic link as the final component." }, + { "SSH_FX_CANNOT_DELETE", + "The file cannot be deleted. One possible reason is that the advisory READONLY attribute-bit is set." }, + { "SSH_FX_INVALID_PARAMETER", + "One of the parameters was out of range, or the parameters specified cannot be used together." }, + { "SSH_FX_FILE_IS_A_DIRECTORY", + "The specified file was a directory in a context where a directory cannot be used." }, + { "SSH_FX_BYTE_RANGE_LOCK_CONFLICT", + " A read or write operation failed because another process's mandatory byte-range lock overlaps with the request." }, + { "SSH_FX_BYTE_RANGE_LOCK_REFUSED", "A request for a byte range lock was refused." }, + { "SSH_FX_DELETE_PENDING", "An operation was attempted on a file for which a delete operation is pending." }, + { "SSH_FX_FILE_CORRUPT", "The file is corrupt; an filesystem integrity check should be run." }, + { "SSH_FX_OWNER_INVALID", "The principal specified can not be assigned as an owner of a file." }, + { "SSH_FX_GROUP_INVALID", "The principal specified can not be assigned as the primary group of a file." }, + { "SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK", + "The requested operation could not be completed because the specifed byte range lock has not been granted." }, + + }; + + public static final String[] getDescription(int errorCode) + { + if ((errorCode < 0) || (errorCode >= messages.length)) + return null; + + return messages[errorCode]; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java new file mode 100644 index 0000000000..3571414ac8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java @@ -0,0 +1,223 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Open Flags. + * + * The following table is provided to assist in mapping POSIX semantics + * to equivalent SFTP file open parameters: + *

+ * TODO: This comment should be moved to the open method. + *

+ *

    + *
  • O_RDONLY + *
    • desired-access = READ_DATA | READ_ATTRIBUTES
    + *
  • + *
+ *
    + *
  • O_WRONLY + *
    • desired-access = WRITE_DATA | WRITE_ATTRIBUTES
    + *
  • + *
+ *
    + *
  • O_RDWR + *
    • desired-access = READ_DATA | READ_ATTRIBUTES | WRITE_DATA | WRITE_ATTRIBUTES
    + *
  • + *
+ *
    + *
  • O_APPEND + *
      + *
    • desired-access = WRITE_DATA | WRITE_ATTRIBUTES | APPEND_DATA
    • + *
    • flags = SSH_FXF_ACCESS_APPEND_DATA and or SSH_FXF_ACCESS_APPEND_DATA_ATOMIC
    • + *
    + *
  • + *
+ *
    + *
  • O_CREAT + *
      + *
    • flags = SSH_FXF_OPEN_OR_CREATE
    • + *
    + *
  • + *
+ *
    + *
  • O_TRUNC + *
      + *
    • flags = SSH_FXF_TRUNCATE_EXISTING
    • + *
    + *
  • + *
+ *
    + *
  • O_TRUNC|O_CREATE + *
      + *
    • flags = SSH_FXF_CREATE_TRUNCATE
    • + *
    + *
  • + *
+ * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: OpenFlags.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class OpenFlags +{ + /** + * Disposition is a 3 bit field that controls how the file is opened. + * The server MUST support these bits (possible enumaration values: + * SSH_FXF_CREATE_NEW, SSH_FXF_CREATE_TRUNCATE, SSH_FXF_OPEN_EXISTING, + * SSH_FXF_OPEN_OR_CREATE or SSH_FXF_TRUNCATE_EXISTING). + */ + public static final int SSH_FXF_ACCESS_DISPOSITION = 0x00000007; + + /** + * A new file is created; if the file already exists, the server + * MUST return status SSH_FX_FILE_ALREADY_EXISTS. + */ + public static final int SSH_FXF_CREATE_NEW = 0x00000000; + + /** + * A new file is created; if the file already exists, it is opened + * and truncated. + */ + public static final int SSH_FXF_CREATE_TRUNCATE = 0x00000001; + + /** + * An existing file is opened. If the file does not exist, the + * server MUST return SSH_FX_NO_SUCH_FILE. If a directory in the + * path does not exist, the server SHOULD return + * SSH_FX_NO_SUCH_PATH. It is also acceptable if the server + * returns SSH_FX_NO_SUCH_FILE in this case. + */ + public static final int SSH_FXF_OPEN_EXISTING = 0x00000002; + + /** + * If the file exists, it is opened. If the file does not exist, + * it is created. + */ + public static final int SSH_FXF_OPEN_OR_CREATE = 0x00000003; + + /** + * An existing file is opened and truncated. If the file does not + * exist, the server MUST return the same error codes as defined + * for SSH_FXF_OPEN_EXISTING. + */ + public static final int SSH_FXF_TRUNCATE_EXISTING = 0x00000004; + + /** + * Data is always written at the end of the file. The offset field + * of the SSH_FXP_WRITE requests are ignored. + *

+ * Data is not required to be appended atomically. This means that + * if multiple writers attempt to append data simultaneously, data + * from the first may be lost. However, data MAY be appended + * atomically. + */ + public static final int SSH_FXF_ACCESS_APPEND_DATA = 0x00000008; + + /** + * Data is always written at the end of the file. The offset field + * of the SSH_FXP_WRITE requests are ignored. + *

+ * Data MUST be written atomically so that there is no chance that + * multiple appenders can collide and result in data being lost. + *

+ * If both append flags are specified, the server SHOULD use atomic + * append if it is available, but SHOULD use non-atomic appends + * otherwise. The server SHOULD NOT fail the request in this case. + */ + public static final int SSH_FXF_ACCESS_APPEND_DATA_ATOMIC = 0x00000010; + + /** + * Indicates that the server should treat the file as text and + * convert it to the canonical newline convention in use. + * (See Determining Server Newline Convention in section 5.3 in the + * SFTP standard draft). + *

+ * When a file is opened with this flag, the offset field in the read + * and write functions is ignored. + *

+ * Servers MUST process multiple, parallel reads and writes correctly + * in this mode. Naturally, it is permissible for them to do this by + * serializing the requests. + *

+ * Clients SHOULD use the SSH_FXF_ACCESS_APPEND_DATA flag to append + * data to a text file rather then using write with a calculated offset. + */ + public static final int SSH_FXF_ACCESS_TEXT_MODE = 0x00000020; + + /** + * The server MUST guarantee that no other handle has been opened + * with ACE4_READ_DATA access, and that no other handle will be + * opened with ACE4_READ_DATA access until the client closes the + * handle. (This MUST apply both to other clients and to other + * processes on the server.) + *

+ * If there is a conflicting lock the server MUST return + * SSH_FX_LOCK_CONFLICT. If the server cannot make the locking + * guarantee, it MUST return SSH_FX_OP_UNSUPPORTED. + *

+ * Other handles MAY be opened for ACE4_WRITE_DATA or any other + * combination of accesses, as long as ACE4_READ_DATA is not included + * in the mask. + */ + public static final int SSH_FXF_ACCESS_BLOCK_READ = 0x00000040; + + /** + * The server MUST guarantee that no other handle has been opened + * with ACE4_WRITE_DATA or ACE4_APPEND_DATA access, and that no other + * handle will be opened with ACE4_WRITE_DATA or ACE4_APPEND_DATA + * access until the client closes the handle. (This MUST apply both + * to other clients and to other processes on the server.) + *

+ * If there is a conflicting lock the server MUST return + * SSH_FX_LOCK_CONFLICT. If the server cannot make the locking + * guarantee, it MUST return SSH_FX_OP_UNSUPPORTED. + *

+ * Other handles MAY be opened for ACE4_READ_DATA or any other + * combination of accesses, as long as neither ACE4_WRITE_DATA nor + * ACE4_APPEND_DATA are included in the mask. + */ + public static final int SSH_FXF_ACCESS_BLOCK_WRITE = 0x00000080; + + /** + * The server MUST guarantee that no other handle has been opened + * with ACE4_DELETE access, opened with the + * SSH_FXF_ACCESS_DELETE_ON_CLOSE flag set, and that no other handle + * will be opened with ACE4_DELETE access or with the + * SSH_FXF_ACCESS_DELETE_ON_CLOSE flag set, and that the file itself + * is not deleted in any other way until the client closes the handle. + *

+ * If there is a conflicting lock the server MUST return + * SSH_FX_LOCK_CONFLICT. If the server cannot make the locking + * guarantee, it MUST return SSH_FX_OP_UNSUPPORTED. + */ + public static final int SSH_FXF_ACCESS_BLOCK_DELETE = 0x00000100; + + /** + * If this bit is set, the above BLOCK modes are advisory. In advisory + * mode, only other accesses that specify a BLOCK mode need be + * considered when determining whether the BLOCK can be granted, + * and the server need not prevent I/O operations that violate the + * block mode. + *

+ * The server MAY perform mandatory locking even if the BLOCK_ADVISORY + * bit is set. + */ + public static final int SSH_FXF_ACCESS_BLOCK_ADVISORY = 0x00000200; + + /** + * If the final component of the path is a symlink, then the open + * MUST fail, and the error SSH_FX_LINK_LOOP MUST be returned. + */ + public static final int SSH_FXF_ACCESS_NOFOLLOW = 0x00000400; + + /** + * The file should be deleted when the last handle to it is closed. + * (The last handle may not be an sftp-handle.) This MAY be emulated + * by a server if the OS doesn't support it by deleting the file when + * this handle is closed. + *

+ * It is implementation specific whether the directory entry is + * removed immediately or when the handle is closed. + */ + public static final int SSH_FXF_ACCESS_DELETE_ON_CLOSE = 0x00000800; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/Packet.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/Packet.java new file mode 100644 index 0000000000..d9e181d53a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/sftp/Packet.java @@ -0,0 +1,43 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Paket Types + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Packet.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class Packet +{ + public static final int SSH_FXP_INIT = 1; + public static final int SSH_FXP_VERSION = 2; + public static final int SSH_FXP_OPEN = 3; + public static final int SSH_FXP_CLOSE = 4; + public static final int SSH_FXP_READ = 5; + public static final int SSH_FXP_WRITE = 6; + public static final int SSH_FXP_LSTAT = 7; + public static final int SSH_FXP_FSTAT = 8; + public static final int SSH_FXP_SETSTAT = 9; + public static final int SSH_FXP_FSETSTAT = 10; + public static final int SSH_FXP_OPENDIR = 11; + public static final int SSH_FXP_READDIR = 12; + public static final int SSH_FXP_REMOVE = 13; + public static final int SSH_FXP_MKDIR = 14; + public static final int SSH_FXP_RMDIR = 15; + public static final int SSH_FXP_REALPATH = 16; + public static final int SSH_FXP_STAT = 17; + public static final int SSH_FXP_RENAME = 18; + public static final int SSH_FXP_READLINK = 19; + public static final int SSH_FXP_SYMLINK = 20; + + public static final int SSH_FXP_STATUS = 101; + public static final int SSH_FXP_HANDLE = 102; + public static final int SSH_FXP_DATA = 103; + public static final int SSH_FXP_NAME = 104; + public static final int SSH_FXP_ATTRS = 105; + + public static final int SSH_FXP_EXTENDED = 200; + public static final int SSH_FXP_EXTENDED_REPLY = 201; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java new file mode 100644 index 0000000000..4ec7c30d94 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java @@ -0,0 +1,253 @@ + +package com.trilead.ssh2.signature; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + + +/** + * DSASHA1Verify. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DSASHA1Verify.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class DSASHA1Verify implements SSHSignature +{ + + private static final Logger log = Logger.getLogger(DSASHA1Verify.class); + public static final String ID_SSH_DSS = "ssh-dss"; + + private static class InstanceHolder { + private static DSASHA1Verify sInstance = new DSASHA1Verify(); + } + + private DSASHA1Verify() { + } + + public static DSASHA1Verify get() { + return InstanceHolder.sInstance; + } + + @Override + public String getKeyFormat() { + return ID_SSH_DSS; + } + + public PublicKey decodePublicKey(byte[] key) throws IOException + { + TypesReader tr = new TypesReader(key); + + String key_format = tr.readString(); + + if (!key_format.equals(DSASHA1Verify.ID_SSH_DSS)) + throw new IllegalArgumentException("This is not a ssh-dss public key!"); + + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + BigInteger g = tr.readMPINT(); + BigInteger y = tr.readMPINT(); + + if (tr.remain() != 0) + throw new IOException("Padding in DSA public key!"); + + try { + KeyFactory kf = KeyFactory.getInstance("DSA"); + + KeySpec ks = new DSAPublicKeySpec(y, p, q, g); + return (DSAPublicKey) kf.generatePublic(ks); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IOException(e); + } + } + + public byte[] encodePublicKey(PublicKey pk) throws IOException + { + DSAPublicKey dsaPublicKey = (DSAPublicKey) pk; + + TypesWriter tw = new TypesWriter(); + + tw.writeString(DSASHA1Verify.ID_SSH_DSS); + + DSAParams params = dsaPublicKey.getParams(); + tw.writeMPInt(params.getP()); + tw.writeMPInt(params.getQ()); + tw.writeMPInt(params.getG()); + tw.writeMPInt(dsaPublicKey.getY()); + + return tw.getBytes(); + } + + /** + * Convert from Java's signature ASN.1 encoding to the SSH spec. + *

+ * Java ASN.1 encoding: + *

+	 * SEQUENCE ::= {
+	 *    r INTEGER,
+	 *    s INTEGER
+	 * }
+	 * 
+ */ + private static byte[] encodeSignature(byte[] ds) + { + TypesWriter tw = new TypesWriter(); + + tw.writeString(ID_SSH_DSS); + + int len, index; + + index = 3; + len = ds[index++] & 0xff; + byte[] r = new byte[len]; + System.arraycopy(ds, index, r, 0, r.length); + + index = index + len + 1; + len = ds[index++] & 0xff; + byte[] s = new byte[len]; + System.arraycopy(ds, index, s, 0, s.length); + + byte[] a40 = new byte[40]; + + /* Patch (unsigned) r and s into the target array. */ + + int r_copylen = (r.length < 20) ? r.length : 20; + int s_copylen = (s.length < 20) ? s.length : 20; + + System.arraycopy(r, r.length - r_copylen, a40, 20 - r_copylen, r_copylen); + System.arraycopy(s, s.length - s_copylen, a40, 40 - s_copylen, s_copylen); + + tw.writeString(a40, 0, 40); + + return tw.getBytes(); + } + + private byte[] decodeSignature(byte[] sig) throws IOException + { + byte[] rsArray = null; + + if (sig.length == 40) + { + /* OK, another broken SSH server. */ + rsArray = sig; + } + else + { + /* Hopefully a server obeying the standard... */ + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + if (!sig_format.equals(DSASHA1Verify.ID_SSH_DSS)) + throw new IOException("Peer sent wrong signature format"); + + rsArray = tr.readByteString(); + + if (rsArray.length != 40) + throw new IOException("Peer sent corrupt signature"); + + if (tr.remain() != 0) + throw new IOException("Padding in DSA signature!"); + } + + int i = 0; + int j = 0; + byte[] tmp; + + if (rsArray[0] == 0 && rsArray[1] == 0 && rsArray[2] == 0) { + j = ((rsArray[i++] << 24) & 0xff000000) | ((rsArray[i++] << 16) & 0x00ff0000) + | ((rsArray[i++] << 8) & 0x0000ff00) | ((rsArray[i++]) & 0x000000ff); + i += j; + j = ((rsArray[i++] << 24) & 0xff000000) | ((rsArray[i++] << 16) & 0x00ff0000) + | ((rsArray[i++] << 8) & 0x0000ff00) | ((rsArray[i++]) & 0x000000ff); + tmp = new byte[j]; + System.arraycopy(rsArray, i, tmp, 0, j); + rsArray = tmp; + } + + /* ASN.1 */ + int frst = ((rsArray[0] & 0x80) != 0 ? 1 : 0); + int scnd = ((rsArray[20] & 0x80) != 0 ? 1 : 0); + + /* Calculate output length */ + int length = rsArray.length + 6 + frst + scnd; + tmp = new byte[length]; + + /* DER-encoding to match Java */ + tmp[0] = (byte) 0x30; + + if (rsArray.length != 40) + throw new IOException("Peer sent corrupt signature"); + /* Calculate length */ + tmp[1] = (byte) 0x2c; + tmp[1] += frst; + tmp[1] += scnd; + + /* First item */ + tmp[2] = (byte) 0x02; + + /* First item length */ + tmp[3] = (byte) 0x14; + tmp[3] += frst; + + /* Copy in the data for first item */ + System.arraycopy(rsArray, 0, tmp, 4 + frst, 20); + + /* Second item */ + tmp[4 + tmp[3]] = (byte) 0x02; + + /* Second item length */ + tmp[5 + tmp[3]] = (byte) 0x14; + tmp[5 + tmp[3]] += scnd; + + /* Copy in the data for the second item */ + System.arraycopy(rsArray, 20, tmp, 6 + tmp[3] + scnd, 20); + + /* Swap buffers */ + rsArray = tmp; + + return rsArray; + } + + public boolean verifySignature(byte[] message, byte[] sshSig, PublicKey dpk) throws IOException + { + byte[] javaSig = decodeSignature(sshSig); + try { + Signature s = Signature.getInstance("SHA1withDSA"); + s.initVerify(dpk); + s.update(message); + return s.verify(javaSig); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IOException("No such algorithm", e); + } catch (SignatureException e) { + throw new IOException(e); + } + } + + public byte[] generateSignature(byte[] message, PrivateKey pk, SecureRandom rnd) throws IOException + { + try { + Signature s = Signature.getInstance("SHA1withDSA"); + s.initSign(pk); + s.update(message); + return encodeSignature(s.sign()); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) { + throw new IOException(e); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java new file mode 100644 index 0000000000..08b6a9f225 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java @@ -0,0 +1,582 @@ +/* + * Copyright 2014 Kenny Root + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.signature; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import com.trilead.ssh2.crypto.SimpleDERReader; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + +/** + * @author Kenny Root + * + */ +public abstract class ECDSASHA2Verify implements SSHSignature { + private static final Logger log = Logger.getLogger(ECDSASHA2Verify.class); + + public static final String ECDSA_SHA2_PREFIX = "ecdsa-sha2-"; + + @Override + public abstract String getKeyFormat(); + + @Override + public PublicKey decodePublicKey(byte[] key) throws IOException { + TypesReader tr = new TypesReader(key); + + String key_format = tr.readString(); + + if (!key_format.startsWith(ECDSA_SHA2_PREFIX)) + throw new IllegalArgumentException("This is not an ECDSA public key"); + + String curveName = tr.readString(); + byte[] groupBytes = tr.readByteString(); + + if (tr.remain() != 0) + throw new IOException("Padding in ECDSA public key!"); + + if (!key_format.equals(getKeyFormat())) { + throw new IOException("Key format is inconsistent with curve name: " + key_format + + " != " + curveName); + } + + ECParameterSpec params = getParameterSpec(); + if (params == null) { + throw new IOException("Curve is not supported: " + curveName); + } + + ECPoint group = decodeECPoint(groupBytes); + if (group == null) { + throw new IOException("Invalid ECDSA group"); + } + + KeySpec keySpec = new ECPublicKeySpec(group, params); + + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nsae) { + throw new IOException("No EC KeyFactory available", nsae); + } + } + + public abstract ECParameterSpec getParameterSpec(); + + @Override + public byte[] encodePublicKey(PublicKey key) { + ECPublicKey ecPublicKey = (ECPublicKey) key; + TypesWriter tw = new TypesWriter(); + + String keyFormat = ECDSA_SHA2_PREFIX + getCurveName(); + + tw.writeString(keyFormat); + + tw.writeString(getCurveName()); + + byte[] encoded = encodeECPoint(ecPublicKey.getW(), ecPublicKey.getParams().getCurve()); + tw.writeString(encoded, 0, encoded.length); + + return tw.getBytes(); + } + + public static ECDSASHA2Verify getVerifierForKey(ECKey key) { + switch (key.getParams().getCurve().getField().getFieldSize()) { + case 256: + return ECDSASHA2NISTP256Verify.get(); + case 384: + return ECDSASHA2NISTP384Verify.get(); + case 521: + return ECDSASHA2NISTP521Verify.get(); + default: + return null; + } + } + + public static String getSshKeyType(ECKey ecKey) { + ECDSASHA2Verify verifier = getVerifierForKey(ecKey); + if (verifier == null) + return null; + return verifier.getKeyFormat(); + } + + public abstract String getCurveName(); + + public abstract String getOid(); + + public static int getCurveSize(ECParameterSpec params) { + return params.getCurve().getField().getFieldSize(); + } + + public static ECDSASHA2Verify getVerifierForOID(String oid) { + if (oid == null) { + return null; + } + + if (oid.equals(ECDSASHA2NISTP256Verify.get().getOid())) { + return ECDSASHA2NISTP256Verify.get(); + } else if (oid.equals(ECDSASHA2NISTP384Verify.get().getOid())) { + return ECDSASHA2NISTP384Verify.get(); + } else if (oid.equals(ECDSASHA2NISTP521Verify.get().getOid())) { + return ECDSASHA2NISTP521Verify.get(); + } else { + return null; + } + } + + private byte[] decodeSSHECDSASignature(byte[] sig) throws IOException { + byte[] rsArray; + + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + if (!sig_format.equals(getKeyFormat())) { + throw new IOException("Unsupported format: " + sig_format); + } + + rsArray = tr.readByteString(); + + if (tr.remain() != 0) + throw new IOException("Padding in ECDSA signature!"); + + byte[] rArray; + byte[] sArray; + { + TypesReader rsReader = new TypesReader(rsArray); + rArray = rsReader.readMPINT().toByteArray(); + sArray = rsReader.readMPINT().toByteArray(); + } + + int first = rArray.length; + int second = sArray.length; + + /* We can't have the high bit set, so add an extra zero at the beginning if so. */ + if ((rArray[0] & 0x80) != 0) { + first++; + } + if ((sArray[0] & 0x80) != 0) { + second++; + } + + /* Calculate total output length */ + ByteArrayOutputStream os = new ByteArrayOutputStream(6 + first + second); + + /* ASN.1 SEQUENCE tag */ + os.write(0x30); + + /* Size of SEQUENCE */ + writeLength(4 + first + second, os); + + /* ASN.1 INTEGER tag */ + os.write(0x02); + + /* "r" INTEGER length */ + writeLength(first, os); + + /* Copy in the "r" INTEGER */ + if (first != rArray.length) { + os.write(0x00); + } + os.write(rArray); + + /* ASN.1 INTEGER tag */ + os.write(0x02); + + /* "s" INTEGER length */ + writeLength(second, os); + + /* Copy in the "s" INTEGER */ + if (second != sArray.length) { + os.write(0x00); + } + os.write(sArray); + + return os.toByteArray(); + } + + private static void writeLength(int length, OutputStream os) throws IOException { + if (length <= 0x7F) { + os.write(length); + return; + } + + int numOctets = 0; + int lenCopy = length; + while (lenCopy != 0) { + lenCopy >>>= 8; + numOctets++; + } + + os.write(0x80 | numOctets); + + for (int i = (numOctets - 1) * 8; i >= 0; i -= 8) { + os.write((byte) (length >> i)); + } + } + + private byte[] encodeSSHECDSASignature(byte[] sig) throws IOException + { + TypesWriter tw = new TypesWriter(); + + tw.writeString(getKeyFormat()); + + /* + * This is a signature in ASN.1 DER format. It should look like: + * 0x30 + * 0x02 + * 0x02 + */ + + SimpleDERReader reader = new SimpleDERReader(sig); + reader.resetInput(reader.readSequenceAsByteArray()); + + BigInteger r = reader.readInt(); + BigInteger s = reader.readInt(); + + // Write the to its own types writer. + TypesWriter rsWriter = new TypesWriter(); + rsWriter.writeMPInt(r); + rsWriter.writeMPInt(s); + byte[] encoded = rsWriter.getBytes(); + tw.writeString(encoded, 0, encoded.length); + + return tw.getBytes(); + } + + @Override + public byte[] generateSignature(byte[] message, PrivateKey pk, SecureRandom secureRandom) throws IOException + { + final String algo = getSignatureAlgorithm(); + + try { + Signature s = Signature.getInstance(algo); + s.initSign(pk, secureRandom); + s.update(message); + return encodeSSHECDSASignature(s.sign()); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } + + protected abstract String getSignatureAlgorithm(); + + @Override + public boolean verifySignature(byte[] message, byte[] sshSig, PublicKey pk) throws IOException + { + byte[] javaSig = decodeSSHECDSASignature(sshSig); + try { + Signature s = Signature.getInstance(getSignatureAlgorithm()); + s.initVerify(pk); + s.update(message); + return s.verify(javaSig); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IOException("No such algorithm", e); + } catch (SignatureException e) { + throw new IOException(e); + } + } + + public static String getDigestAlgorithmForParams(ECKey key) { + ECDSASHA2Verify verifier = getVerifierForKey(key); + if (verifier == null) + return null; + return verifier.getDigestAlgorithm(); + } + + protected abstract String getDigestAlgorithm(); + + /** + * Decode an OctetString to EllipticCurvePoint according to SECG 2.3.4 + */ + public ECPoint decodeECPoint(byte[] M) { + if (M.length == 0) { + return null; + } + + // M has len 2 ceil(log_2(q)/8) + 1 ? + EllipticCurve curve = getParameterSpec().getCurve(); + int elementSize = (curve.getField().getFieldSize() + 7) / 8; + if (M.length != 2 * elementSize + 1) { + return null; + } + + // step 3.2 + if (M[0] != 0x04) { + return null; + } + + // Step 3.3 + byte[] xp = new byte[elementSize]; + System.arraycopy(M, 1, xp, 0, elementSize); + + // Step 3.4 + byte[] yp = new byte[elementSize]; + System.arraycopy(M, 1 + elementSize, yp, 0, elementSize); + + ECPoint P = new ECPoint(new BigInteger(1, xp), new BigInteger(1, yp)); + + // TODO check point 3.5 + + // Step 3.6 + return P; + } + + /** + * Encode EllipticCurvePoint to an OctetString + */ + public static byte[] encodeECPoint(ECPoint group, EllipticCurve curve) + { + // M has len 2 ceil(log_2(q)/8) + 1 ? + int elementSize = (curve.getField().getFieldSize() + 7) / 8; + byte[] M = new byte[2 * elementSize + 1]; + + // Uncompressed format + M[0] = 0x04; + + { + byte[] affineX = removeLeadingZeroes(group.getAffineX().toByteArray()); + System.arraycopy(affineX, 0, M, 1 + elementSize - affineX.length, affineX.length); + } + + { + byte[] affineY = removeLeadingZeroes(group.getAffineY().toByteArray()); + System.arraycopy(affineY, 0, M, 1 + elementSize + elementSize - affineY.length, + affineY.length); + } + + return M; + } + + private static byte[] removeLeadingZeroes(byte[] input) { + if (input[0] != 0x00) { + return input; + } + + int pos = 1; + while (pos < input.length - 1 && input[pos] == 0x00) { + pos++; + } + + byte[] output = new byte[input.length - pos]; + System.arraycopy(input, pos, output, 0, output.length); + return output; + } + + public static class ECDSASHA2NISTP256Verify extends ECDSASHA2Verify { + private static final String NISTP256 = "nistp256"; + private static final String NISTP256_OID = "1.2.840.10045.3.1.7"; + private static final String KEY_FORMAT = ECDSA_SHA2_PREFIX + NISTP256; + + @Override + public String getCurveName() { + return NISTP256; + } + + @Override + public String getOid() { + return NISTP256_OID; + } + + @Override + protected String getSignatureAlgorithm() { + return "SHA256withECDSA"; + } + + @Override + protected String getDigestAlgorithm() { + return "SHA-256"; + } + + @Override + public String getKeyFormat() { + return KEY_FORMAT; + } + + @Override + public ECParameterSpec getParameterSpec() { + return nistp256; + } + + private static class InstanceHolder { + private static final ECDSASHA2NISTP256Verify sInstance = new ECDSASHA2NISTP256Verify(); + } + + private ECDSASHA2NISTP256Verify() { + } + + public static ECDSASHA2NISTP256Verify get() { + return ECDSASHA2NISTP256Verify.InstanceHolder.sInstance; + } + + public static ECParameterSpec nistp256 = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16)), + new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16), + new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16)), + new ECPoint(new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16), + new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16)), + new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16), + 1); + } + + public static class ECDSASHA2NISTP384Verify extends ECDSASHA2Verify { + private static final String NISTP384 = "nistp384"; + private static final String NISTP384_OID = "1.3.132.0.34"; + private static final String KEY_FORMAT = ECDSA_SHA2_PREFIX + NISTP384; + + @Override + public String getKeyFormat() { + return KEY_FORMAT; + } + + private static class InstanceHolder { + private static final ECDSASHA2NISTP384Verify sInstance = new ECDSASHA2NISTP384Verify(); + } + + private ECDSASHA2NISTP384Verify() { + } + + public static ECDSASHA2NISTP384Verify get() { + return ECDSASHA2NISTP384Verify.InstanceHolder.sInstance; + } + + @Override + public ECParameterSpec getParameterSpec() { + return nistp384; + } + + @Override + public String getCurveName() { + return NISTP384; + } + + @Override + public String getOid() { + return NISTP384_OID; + } + + @Override + protected String getSignatureAlgorithm() { + return "SHA384withECDSA"; + } + + @Override + protected String getDigestAlgorithm() { + return "SHA-384"; + } + + public static ECParameterSpec nistp384 = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16), + new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16)), + new ECPoint(new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16), + new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16), + 1); + } + + public static class ECDSASHA2NISTP521Verify extends ECDSASHA2Verify { + private static final String NISTP521 = "nistp521"; + private static final String NISTP521_OID = "1.3.132.0.35"; + private static final String KEY_FORMAT = ECDSA_SHA2_PREFIX + NISTP521; + + @Override + public String getKeyFormat() { + return KEY_FORMAT; + } + + @Override + public ECParameterSpec getParameterSpec() { + return nistp521; + } + + @Override + public String getCurveName() { + return NISTP521; + } + + @Override + public String getOid() { + return NISTP521_OID; + } + + @Override + protected String getSignatureAlgorithm() { + return "SHA512withECDSA"; + } + + @Override + protected String getDigestAlgorithm() { + return "SHA-512"; + } + + private static class InstanceHolder { + private static final ECDSASHA2NISTP521Verify sInstance = new ECDSASHA2NISTP521Verify(); + } + + private ECDSASHA2NISTP521Verify() { + } + + public static ECDSASHA2NISTP521Verify get() { + return ECDSASHA2NISTP521Verify.InstanceHolder.sInstance; + } + + public static ECParameterSpec nistp521 = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16)), + new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC", 16), + new BigInteger("0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00", 16)), + new ECPoint(new BigInteger("00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66", 16), + new BigInteger("011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650", 16)), + new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", 16), + 1); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/Ed25519Verify.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/Ed25519Verify.java new file mode 100644 index 0000000000..638fc961bd --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/Ed25519Verify.java @@ -0,0 +1,161 @@ +/* + * Copyright 2015 Kenny Root + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * a.) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * b.) Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * c.) Neither the name of Trilead nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.trilead.ssh2.signature; + +import com.google.crypto.tink.subtle.Ed25519Sign; +import com.trilead.ssh2.crypto.keys.Ed25519PrivateKey; +import com.trilead.ssh2.crypto.keys.Ed25519PublicKey; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; + +/** + * @author Kenny Root + */ +public class Ed25519Verify implements SSHSignature { + private static final Logger log = Logger.getLogger(Ed25519Verify.class); + + /** Identifies this as an Ed25519 key in the protocol. */ + public static final String ED25519_ID = "ssh-ed25519"; + + private static final int ED25519_PK_SIZE_BYTES = 32; + private static final int ED25519_SIG_SIZE_BYTES = 64; + + private static class InstanceHolder { + private static final Ed25519Verify sInstance = new Ed25519Verify(); + } + + private Ed25519Verify() { + } + + public static Ed25519Verify get() { + return Ed25519Verify.InstanceHolder.sInstance; + } + + @Override + public byte[] encodePublicKey(PublicKey publicKey) { + Ed25519PublicKey ed25519PublicKey = (Ed25519PublicKey) publicKey; + + TypesWriter tw = new TypesWriter(); + + tw.writeString(ED25519_ID); + byte[] encoded = ed25519PublicKey.getAbyte(); + tw.writeString(encoded, 0, encoded.length); + + return tw.getBytes(); + } + + @Override + public PublicKey decodePublicKey(byte[] encoded) throws IOException { + TypesReader tr = new TypesReader(encoded); + + String key_format = tr.readString(); + if (!key_format.equals(ED25519_ID)) { + throw new IOException("This is not an Ed25519 key"); + } + + byte[] keyBytes = tr.readByteString(); + + if (tr.remain() != 0) { + throw new IOException("Padding in Ed25519 public key! " + tr.remain() + " bytes left."); + } + + if (keyBytes.length != ED25519_PK_SIZE_BYTES) { + throw new IOException("Ed25519 was not of correct length: " + keyBytes.length + " vs " + ED25519_PK_SIZE_BYTES); + } + + return new Ed25519PublicKey(keyBytes); + } + + @Override + public byte[] generateSignature(byte[] msg, PrivateKey privateKey, SecureRandom secureRandom) throws IOException { + Ed25519PrivateKey ed25519PrivateKey = (Ed25519PrivateKey) privateKey; + try { + return encodeSSHEd25519Signature(new Ed25519Sign(ed25519PrivateKey.getSeed()).sign(msg)); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + + @Override + public boolean verifySignature(byte[] message, byte[] sshSig, PublicKey publicKey) throws IOException { + Ed25519PublicKey ed25519PublicKey = (Ed25519PublicKey) publicKey; + byte[] javaSig = decodeSSHEd25519Signature(sshSig); + try { + new com.google.crypto.tink.subtle.Ed25519Verify(ed25519PublicKey.getAbyte()).verify(javaSig, message); + return true; + } catch (GeneralSecurityException e) { + return false; + } + } + + private static byte[] encodeSSHEd25519Signature(byte[] sig) { + TypesWriter tw = new TypesWriter(); + + tw.writeString(ED25519_ID); + tw.writeString(sig, 0, sig.length); + + return tw.getBytes(); + } + + private static byte[] decodeSSHEd25519Signature(byte[] sig) throws IOException { + byte[] rsArray; + + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + if (!sig_format.equals(ED25519_ID)) { + throw new IOException("Peer sent wrong signature format"); + } + + rsArray = tr.readByteString(); + + if (tr.remain() != 0) { + throw new IOException("Padding in Ed25519 signature!"); + } + + if (rsArray.length > ED25519_SIG_SIZE_BYTES) { + throw new IOException("Ed25519 signature was " + rsArray.length + " bytes (" + ED25519_PK_SIZE_BYTES + " expected)"); + } + + return rsArray; + } + + @Override + public String getKeyFormat() { + return ED25519_ID; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java new file mode 100644 index 0000000000..8acf6e0203 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java @@ -0,0 +1,164 @@ + +package com.trilead.ssh2.signature; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPublicKeySpec; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + + +/** + * RSASHA1Verify. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RSASHA1Verify.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class RSASHA1Verify implements SSHSignature +{ + private static final Logger log = Logger.getLogger(RSASHA1Verify.class); + public static final String ID_SSH_RSA = "ssh-rsa"; + + private static class InstanceHolder { + private static RSASHA1Verify sInstance = new RSASHA1Verify(); + } + + private RSASHA1Verify() { + } + + public static RSASHA1Verify get() { + return RSASHA1Verify.InstanceHolder.sInstance; + } + + @Override + public String getKeyFormat() { + return ID_SSH_RSA; + } + + public PublicKey decodePublicKey(byte[] key) throws IOException + { + TypesReader tr = new TypesReader(key); + + String key_format = tr.readString(); + + if (!key_format.equals(ID_SSH_RSA)) + throw new IllegalArgumentException("This is not a ssh-rsa public key"); + + BigInteger e = tr.readMPINT(); + BigInteger n = tr.readMPINT(); + + if (tr.remain() != 0) + throw new IOException("Padding in RSA public key!"); + + KeySpec keySpec = new RSAPublicKeySpec(n, e); + + try { + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nsae) { + throw new IOException("No RSA KeyFactory available", nsae); + } + } + + public byte[] encodePublicKey(PublicKey pk) throws IOException + { + RSAPublicKey rsaPublicKey = (RSAPublicKey) pk; + + TypesWriter tw = new TypesWriter(); + + tw.writeString(ID_SSH_RSA); + tw.writeMPInt(rsaPublicKey.getPublicExponent()); + tw.writeMPInt(rsaPublicKey.getModulus()); + + return tw.getBytes(); + } + + private static byte[] decodeSignature(byte[] sig) throws IOException + { + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + + if (!sig_format.equals(ID_SSH_RSA)) + throw new IOException("Peer sent wrong signature format"); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." See also below. + */ + + byte[] s = tr.readByteString(); + + if (s.length == 0) + throw new IOException("Error in RSA signature, S is empty."); + + if (log.isEnabled()) + { + log.log(80, "Decoding ssh-rsa signature string (length: " + s.length + ")"); + } + + if (tr.remain() != 0) + throw new IOException("Padding in RSA signature!"); + + return s; + } + + private static byte[] encodeSignature(byte[] s) throws IOException + { + TypesWriter tw = new TypesWriter(); + + tw.writeString(ID_SSH_RSA); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." + */ + + /* Remove first zero sign byte, if present */ + + if ((s.length > 1) && (s[0] == 0x00)) + tw.writeString(s, 1, s.length - 1); + else + tw.writeString(s, 0, s.length); + + return tw.getBytes(); + } + + public byte[] generateSignature(byte[] message, PrivateKey pk, SecureRandom secureRandom) throws IOException + { + try { + Signature s = Signature.getInstance("SHA1withRSA"); + s.initSign(pk, secureRandom); + s.update(message); + return encodeSignature(s.sign()); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } + + public boolean verifySignature(byte[] message, byte[] sshSig, PublicKey dpk) throws IOException + { + byte[] javaSig = decodeSignature(sshSig); + try { + Signature s = Signature.getInstance("SHA1withRSA"); + s.initVerify(dpk); + s.update(message); + return s.verify(javaSig); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA256Verify.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA256Verify.java new file mode 100644 index 0000000000..01871d00e2 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA256Verify.java @@ -0,0 +1,124 @@ +package com.trilead.ssh2.signature; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; + +public class RSASHA256Verify implements SSHSignature +{ + private static final Logger log = Logger.getLogger(RSASHA256Verify.class); + public static final String ID_RSA_SHA_2_256 = "rsa-sha2-256"; + + private static class InstanceHolder { + private static RSASHA256Verify sInstance = new RSASHA256Verify(); + } + + private RSASHA256Verify() { + } + + public static RSASHA256Verify get() { + return RSASHA256Verify.InstanceHolder.sInstance; + } + + private static byte[] decodeRSASHA256Signature(byte[] sig) throws IOException + { + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + + if (!sig_format.equals(ID_RSA_SHA_2_256)) + throw new IOException("Peer sent wrong signature format"); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." See also below. + */ + + byte[] s = tr.readByteString(); + + if (s.length == 0) + throw new IOException("Error in RSA signature, S is empty."); + + if (log.isEnabled()) + { + log.log(80, "Decoding rsa-sha2-256 signature string (length: " + s.length + ")"); + } + + if (tr.remain() != 0) + throw new IOException("Padding in RSA signature!"); + + return s; + } + + private static byte[] encodeRSASHA256Signature(byte[] s) throws IOException + { + TypesWriter tw = new TypesWriter(); + + tw.writeString(ID_RSA_SHA_2_256); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." + */ + + /* Remove first zero sign byte, if present */ + + if ((s.length > 1) && (s[0] == 0x00)) + tw.writeString(s, 1, s.length - 1); + else + tw.writeString(s, 0, s.length); + + return tw.getBytes(); + } + + @Override + public byte[] generateSignature(byte[] message, PrivateKey privateKey, SecureRandom secureRandom) throws IOException + { + try { + Signature s = Signature.getInstance("SHA256withRSA"); + s.initSign(privateKey, secureRandom); + s.update(message); + return encodeRSASHA256Signature(s.sign()); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } + + @Override + public String getKeyFormat() { + return ID_RSA_SHA_2_256; + } + + @Override + public PublicKey decodePublicKey(byte[] encoded) throws IOException { + return RSASHA1Verify.get().decodePublicKey(encoded); + } + + @Override + public byte[] encodePublicKey(PublicKey publicKey) throws IOException { + return RSASHA1Verify.get().encodePublicKey(publicKey); + } + + @Override + public boolean verifySignature(byte[] message, byte[] sshSig, PublicKey dpk) throws IOException + { + byte[] javaSig = decodeRSASHA256Signature(sshSig); + + try { + Signature s = Signature.getInstance("SHA256withRSA"); + s.initVerify(dpk); + s.update(message); + return s.verify(javaSig); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA512Verify.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA512Verify.java new file mode 100644 index 0000000000..7368b3f660 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/RSASHA512Verify.java @@ -0,0 +1,124 @@ +package com.trilead.ssh2.signature; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; + +public class RSASHA512Verify implements SSHSignature +{ + private static final Logger log = Logger.getLogger(RSASHA512Verify.class); + public static final String ID_RSA_SHA_2_512 = "rsa-sha2-512"; + + private static class InstanceHolder { + private static final RSASHA512Verify sInstance = new RSASHA512Verify(); + } + + private RSASHA512Verify() { + } + + public static RSASHA512Verify get() { + return RSASHA512Verify.InstanceHolder.sInstance; + } + + private static byte[] decodeRSASHA512Signature(byte[] sig) throws IOException + { + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + + if (!sig_format.equals(ID_RSA_SHA_2_512)) + throw new IOException("Peer sent wrong signature format"); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." See also below. + */ + + byte[] s = tr.readByteString(); + + if (s.length == 0) + throw new IOException("Error in RSA signature, S is empty."); + + if (log.isEnabled()) + { + log.log(80, "Decoding rsa-sha2-512 signature string (length: " + s.length + ")"); + } + + if (tr.remain() != 0) + throw new IOException("Padding in RSA signature!"); + + return s; + } + + private static byte[] encodeRSASHA512Signature(byte[] s) + { + TypesWriter tw = new TypesWriter(); + + tw.writeString(ID_RSA_SHA_2_512); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." + */ + + /* Remove first zero sign byte, if present */ + + if ((s.length > 1) && (s[0] == 0x00)) + tw.writeString(s, 1, s.length - 1); + else + tw.writeString(s, 0, s.length); + + return tw.getBytes(); + } + + @Override + public byte[] generateSignature(byte[] message, PrivateKey privateKey, SecureRandom secureRandom) throws IOException + { + try { + // Android's Signature is guaranteed to support this instance + Signature s = Signature.getInstance("SHA512withRSA"); + s.initSign(privateKey, secureRandom); + s.update(message); + return encodeRSASHA512Signature(s.sign()); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } + + @Override + public String getKeyFormat() { + return ID_RSA_SHA_2_512; + } + + @Override + public PublicKey decodePublicKey(byte[] encoded) throws IOException { + return RSASHA1Verify.get().decodePublicKey(encoded); + } + + @Override + public byte[] encodePublicKey(PublicKey publicKey) throws IOException { + return RSASHA1Verify.get().encodePublicKey(publicKey); + } + + @Override + public boolean verifySignature(byte[] message, byte[] sshSig, PublicKey publicKey) throws IOException + { + byte[] javaSig = decodeRSASHA512Signature(sshSig); + try { + Signature s = Signature.getInstance("SHA512withRSA"); + s.initVerify(publicKey); + s.update(message); + return s.verify(javaSig); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IOException(e); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/SSHSignature.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/SSHSignature.java new file mode 100644 index 0000000000..f6fe58d70a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/signature/SSHSignature.java @@ -0,0 +1,23 @@ +package com.trilead.ssh2.signature; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; + +public interface SSHSignature { + /** Returns the supported signature formats. */ + String getKeyFormat(); + + /** Decode from SSH specification key to Java public key. */ + PublicKey decodePublicKey(byte[] encoded) throws IOException; + + /** Encode from Java public key to SSH specification. */ + byte[] encodePublicKey(PublicKey publicKey) throws IOException; + + /** Verifies a SSH-format signature for a given key. */ + boolean verifySignature(byte[] message, byte[] signature, PublicKey publicKey) throws IOException; + + /** Generate an SSH-format signature for the message and private key. */ + byte[] generateSignature(byte[] message, PrivateKey privateKey, SecureRandom secureRandom) throws IOException; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java new file mode 100644 index 0000000000..c83dd15676 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java @@ -0,0 +1,127 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import com.trilead.ssh2.Connection; + +/** + * ClientServerHello. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ClientServerHello.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class ClientServerHello +{ + String server_line; + String client_line; + + String server_versioncomment; + + public final static int readLineRN(InputStream is, byte[] buffer) throws IOException + { + int pos = 0; + boolean need10 = false; + int len = 0; + while (true) + { + int c = is.read(); + if (c == -1) + throw new IOException("Premature connection close"); + + buffer[pos++] = (byte) c; + + if (c == 13) + { + need10 = true; + continue; + } + + if (c == 10) + break; + + if (need10) + throw new IOException("Malformed line sent by the server, the line does not end correctly."); + + len++; + if (pos >= buffer.length) + throw new IOException("The server sent a too long line."); + } + + return len; + } + + public ClientServerHello(InputStream bi, OutputStream bo) throws IOException + { + client_line = "SSH-2.0-" + Connection.identification; + + try { + bo.write((client_line + "\r\n").getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + bo.write((client_line + "\r\n").getBytes()); + } + bo.flush(); + + byte[] serverVersion = new byte[512]; + + for (int i = 0; i < 50; i++) + { + int len = readLineRN(bi, serverVersion); + + try { + server_line = new String(serverVersion, 0, len, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + server_line = new String(serverVersion, 0, len); + } + + if (server_line.startsWith("SSH-")) + break; + } + + if (!server_line.startsWith("SSH-")) + throw new IOException( + "Malformed server identification string. There was no line starting with 'SSH-' amongst the first 50 lines."); + + if (server_line.startsWith("SSH-1.99-")) + server_versioncomment = server_line.substring(9); + else if (server_line.startsWith("SSH-2.0-")) + server_versioncomment = server_line.substring(8); + else + throw new IOException("Server uses incompatible protocol, it is not SSH-2 compatible."); + } + + /** + * @return Returns the client_versioncomment. + */ + public byte[] getClientString() + { + byte[] result; + + try { + result = client_line.getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + result = client_line.getBytes(); + } + + return result; + } + + /** + * @return Returns the server_versioncomment. + */ + public byte[] getServerString() + { + byte[] result; + + try { + result = server_line.getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + result = server_line.getBytes(); + } + + return result; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexManager.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexManager.java new file mode 100644 index 0000000000..f379392dd4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexManager.java @@ -0,0 +1,730 @@ + +package com.trilead.ssh2.transport; + +import com.trilead.ssh2.signature.RSASHA256Verify; +import com.trilead.ssh2.signature.RSASHA512Verify; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.ExtendedServerHostKeyVerifier; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.compression.CompressionFactory; +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.KeyMaterial; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.dh.Curve25519Exchange; +import com.trilead.ssh2.crypto.dh.DhGroupExchange; +import com.trilead.ssh2.crypto.dh.GenericDhExchange; +import com.trilead.ssh2.crypto.digest.HMAC; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.crypto.digest.MACs; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketKexDHInit; +import com.trilead.ssh2.packets.PacketKexDHReply; +import com.trilead.ssh2.packets.PacketKexDhGexGroup; +import com.trilead.ssh2.packets.PacketKexDhGexInit; +import com.trilead.ssh2.packets.PacketKexDhGexReply; +import com.trilead.ssh2.packets.PacketKexDhGexRequest; +import com.trilead.ssh2.packets.PacketKexDhGexRequestOld; +import com.trilead.ssh2.packets.PacketKexInit; +import com.trilead.ssh2.packets.PacketNewKeys; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.Ed25519Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; +import com.trilead.ssh2.signature.SSHSignature; + +/** + * KexManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexManager.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KexManager +{ + private static final Logger log = Logger.getLogger(KexManager.class); + + private static final boolean supportsEc; + static { + KeyFactory keyFact; + try { + keyFact = KeyFactory.getInstance("EC"); + } catch (NoSuchAlgorithmException ignored) { + keyFact = null; + log.log(10, "Disabling EC support due to lack of KeyFactory"); + } + supportsEc = keyFact != null; + } + + private static final Set HOSTKEY_ALGS = new LinkedHashSet<>(); + static { + HOSTKEY_ALGS.add(Ed25519Verify.ED25519_ID); + if (supportsEc) { + HOSTKEY_ALGS.add("ecdsa-sha2-nistp256"); + HOSTKEY_ALGS.add("ecdsa-sha2-nistp384"); + HOSTKEY_ALGS.add("ecdsa-sha2-nistp521"); + } + HOSTKEY_ALGS.add(RSASHA512Verify.ID_RSA_SHA_2_512); + HOSTKEY_ALGS.add(RSASHA256Verify.ID_RSA_SHA_2_256); + HOSTKEY_ALGS.add(RSASHA1Verify.ID_SSH_RSA); + HOSTKEY_ALGS.add(DSASHA1Verify.ID_SSH_DSS); + } + + private static final Set KEX_ALGS = new LinkedHashSet<>(); + static { + KEX_ALGS.add(Curve25519Exchange.NAME); + KEX_ALGS.add(Curve25519Exchange.ALT_NAME); + if (supportsEc) { + KEX_ALGS.add("ecdh-sha2-nistp256"); + KEX_ALGS.add("ecdh-sha2-nistp384"); + KEX_ALGS.add("ecdh-sha2-nistp521"); + } + KEX_ALGS.add("diffie-hellman-group18-sha512"); + KEX_ALGS.add("diffie-hellman-group16-sha512"); + KEX_ALGS.add("diffie-hellman-group-exchange-sha256"); + KEX_ALGS.add("diffie-hellman-group14-sha256"); + KEX_ALGS.add("diffie-hellman-group-exchange-sha1"); + KEX_ALGS.add("diffie-hellman-group14-sha1"); + KEX_ALGS.add("diffie-hellman-group1-sha1"); + + // Indicate client support for ext-info + KEX_ALGS.add("ext-info-c"); + } + + private KexState kxs; + private int kexCount = 0; + private KeyMaterial km; + byte[] sessionId; + private ClientServerHello csh; + + private final Object accessLock = new Object(); + private ConnectionInfo lastConnInfo = null; + + private boolean connectionClosed = false; + + private boolean ignore_next_kex_packet = false; + + private final TransportManager tm; + + private CryptoWishList nextKEXcryptoWishList; + private DHGexParameters nextKEXdhgexParameters; + + private ServerHostKeyVerifier verifier; + private final String hostname; + private final int port; + private final SecureRandom rnd; + + public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port, + ServerHostKeyVerifier keyVerifier, SecureRandom rnd) + { + this.tm = tm; + this.csh = csh; + this.nextKEXcryptoWishList = initialCwl; + this.nextKEXdhgexParameters = new DHGexParameters(); + this.hostname = hostname; + this.port = port; + this.verifier = keyVerifier; + this.rnd = rnd; + } + + public ConnectionInfo getOrWaitForConnectionInfo(int minKexCount) throws IOException + { + synchronized (accessLock) + { + while (true) + { + if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount)) + return lastConnInfo; + + if (connectionClosed) + throw new IOException("Key exchange was not finished, connection is closed.", tm.getReasonClosedCause()); + + try + { + accessLock.wait(); + } + catch (InterruptedException ignore) + { + } + } + } + } + + private String getFirstMatch(String[] client, String[] server) throws NegotiateException + { + if (client == null || server == null) + throw new IllegalArgumentException(); + + if (client.length == 0) + return null; + + for (String aClient : client) { + for (String aServer : server) { + if (aClient.equals(aServer)) + return aClient; + } + } + throw new NegotiateException(); + } + + private boolean compareFirstOfNameList(String[] a, String[] b) + { + if (a == null || b == null) + throw new IllegalArgumentException(); + + if ((a.length == 0) && (b.length == 0)) + return true; + + if ((a.length == 0) || (b.length == 0)) + return false; + + return (a[0].equals(b[0])); + } + + private boolean isGuessOK(KexParameters cpar, KexParameters spar) + { + if (cpar == null || spar == null) + throw new IllegalArgumentException(); + + if (!compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms)) + { + return false; + } + + return compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms); + } + + private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server) + { + NegotiatedParameters np = new NegotiatedParameters(); + + try + { + np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms); + + log.log(20, "kex_algo=" + np.kex_algo); + + np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms, + server.server_host_key_algorithms); + + log.log(20, "server_host_key_algo=" + np.server_host_key_algo); + + np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server, + server.encryption_algorithms_client_to_server); + np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client, + server.encryption_algorithms_server_to_client); + + log.log(20, "enc_algo_client_to_server=" + np.enc_algo_client_to_server); + log.log(20, "enc_algo_server_to_client=" + np.enc_algo_server_to_client); + + np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server, + server.mac_algorithms_client_to_server); + np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client, + server.mac_algorithms_server_to_client); + + log.log(20, "mac_algo_client_to_server=" + np.mac_algo_client_to_server); + log.log(20, "mac_algo_server_to_client=" + np.mac_algo_server_to_client); + + np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server, + server.compression_algorithms_client_to_server); + np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client, + server.compression_algorithms_server_to_client); + + log.log(20, "comp_algo_client_to_server=" + np.comp_algo_client_to_server); + log.log(20, "comp_algo_server_to_client=" + np.comp_algo_server_to_client); + + } + catch (NegotiateException e) + { + return null; + } + + try + { + np.lang_client_to_server = getFirstMatch(client.languages_client_to_server, + server.languages_client_to_server); + } + catch (NegotiateException e1) + { + np.lang_client_to_server = null; + } + + try + { + np.lang_server_to_client = getFirstMatch(client.languages_server_to_client, + server.languages_server_to_client); + } + catch (NegotiateException e2) + { + np.lang_server_to_client = null; + } + + if (isGuessOK(client, server)) + np.guessOK = true; + + return np; + } + + public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) throws IOException + { + nextKEXcryptoWishList = cwl; + filterHostKeyTypes(nextKEXcryptoWishList); + + nextKEXdhgexParameters = dhgex; + + if (kxs == null) + { + kxs = new KexState(); + + kxs.dhgexParameters = nextKEXdhgexParameters; + PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList); + kxs.localKEX = kp; + tm.sendKexMessage(kp.getPayload()); + } + } + + /** + * If the verifier can indicate which algorithms it knows about for this host, then + * filter out our crypto wish list to only include those algorithms. Otherwise we'll + * negotiate a host key we have not previously confirmed. + * + * @param cwl crypto wish list to filter + */ + private void filterHostKeyTypes(CryptoWishList cwl) { + if (verifier instanceof ExtendedServerHostKeyVerifier) { + ExtendedServerHostKeyVerifier extendedVerifier = (ExtendedServerHostKeyVerifier) verifier; + + List knownAlgorithms = extendedVerifier.getKnownKeyAlgorithmsForHost(hostname, port); + if (knownAlgorithms != null && knownAlgorithms.size() > 0) { + ArrayList filteredAlgorithms = new ArrayList<>(knownAlgorithms.size()); + + /* + * Look at our current wish list and adjust it based on what the client already knows, but + * be careful to keep it in the order desired by the wish list. + */ + for (String capableAlgo : cwl.serverHostKeyAlgorithms) { + for (String knownAlgo : knownAlgorithms) { + if (capableAlgo.equals(knownAlgo)) { + filteredAlgorithms.add(knownAlgo); + } + } + } + + if (filteredAlgorithms.size() > 0) { + cwl.serverHostKeyAlgorithms = filteredAlgorithms.toArray(new String[0]); + } + } + } + } + + private void establishKeyMaterial() throws IOException + { + try + { + int mac_cs_key_len = MACs.getKeyLen(kxs.np.mac_algo_client_to_server); + int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server); + int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server); + + int mac_sc_key_len = MACs.getKeyLen(kxs.np.mac_algo_server_to_client); + int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client); + int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client); + + km = KeyMaterial.create(kxs.hashAlgo, kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len, + enc_sc_key_len, enc_sc_block_len, mac_sc_key_len); + } + catch (IllegalArgumentException e) + { + throw new IOException("Could not establish key material: " + e.getMessage()); + } + } + + private void finishKex() throws IOException + { + if (sessionId == null) + sessionId = kxs.H; + + establishKeyMaterial(); + + /* Tell the other side that we start using the new material */ + + PacketNewKeys ign = new PacketNewKeys(); + tm.sendKexMessage(ign.getPayload()); + + BlockCipher cbc; + MAC mac; + ICompressor comp; + + try + { + cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server, + km.initial_iv_client_to_server); + + mac = new HMAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server); + + comp = CompressionFactory.createCompressor(kxs.np.comp_algo_client_to_server); + + } + catch (IllegalArgumentException e1) + { + throw new IOException("Fatal error during MAC startup!"); + } + + tm.changeSendCipher(cbc, mac); + tm.changeSendCompression(comp); + tm.kexFinished(); + } + + public static String[] getDefaultServerHostkeyAlgorithmList() + { + return HOSTKEY_ALGS.toArray(new String[0]); + } + + public static void checkServerHostkeyAlgorithmsList(String[] algos) + { + for (String algo : algos) { + if (!HOSTKEY_ALGS.contains(algo)) + throw new IllegalArgumentException("Unknown server host key algorithm '" + algo + "'"); + } + } + + public static String[] getDefaultKexAlgorithmList() + { + return KEX_ALGS.toArray(new String[0]); + } + + public static void checkKexAlgorithmList(String[] algos) + { + for (String algo : algos) { + if (!KEX_ALGS.contains(algo)) + throw new IllegalArgumentException("Unknown kex algorithm '" + algo + "'"); + } + } + + private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException { + SSHSignature sshSignature; + if (kxs.np.server_host_key_algo.equals(Ed25519Verify.get().getKeyFormat())) { + sshSignature = Ed25519Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get().getKeyFormat())) { + sshSignature = ECDSASHA2Verify.ECDSASHA2NISTP256Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get().getKeyFormat())) { + sshSignature = ECDSASHA2Verify.ECDSASHA2NISTP384Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get().getKeyFormat())) { + sshSignature = ECDSASHA2Verify.ECDSASHA2NISTP521Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(RSASHA512Verify.get().getKeyFormat())) { + sshSignature = RSASHA512Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(RSASHA256Verify.get().getKeyFormat())) { + sshSignature = RSASHA256Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(RSASHA1Verify.get().getKeyFormat())) { + sshSignature = RSASHA1Verify.get(); + } else if (kxs.np.server_host_key_algo.equals(DSASHA1Verify.get().getKeyFormat())) { + sshSignature = DSASHA1Verify.get(); + } else { + throw new IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'"); + } + + PublicKey publicKey = sshSignature.decodePublicKey(hostkey); + log.log(50, "Verifying " + sshSignature.getKeyFormat() + " signature"); + return sshSignature.verifySignature(kxs.H, sig, publicKey); + } + + public synchronized void handleMessage(byte[] msg, int msglen) throws IOException + { + PacketKexInit kip; + + if (msg == null) + { + synchronized (accessLock) + { + connectionClosed = true; + accessLock.notifyAll(); + return; + } + } + + if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT)) + throw new IOException("Unexpected KEX message (type " + msg[0] + ")"); + + if (ignore_next_kex_packet) + { + ignore_next_kex_packet = false; + return; + } + + if (msg[0] == Packets.SSH_MSG_KEXINIT) + { + if ((kxs != null) && (kxs.state != 0)) + throw new IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!"); + + if (kxs == null) + { + /* + * Ah, OK, peer wants to do KEX. Let's be nice and play + * together. + */ + kxs = new KexState(); + kxs.dhgexParameters = nextKEXdhgexParameters; + kip = new PacketKexInit(nextKEXcryptoWishList); + kxs.localKEX = kip; + tm.sendKexMessage(kip.getPayload()); + } + + kip = new PacketKexInit(msg, 0, msglen); + kxs.remoteKEX = kip; + + kxs.np = mergeKexParameters(kxs.localKEX.getKexParameters(), kxs.remoteKEX.getKexParameters()); + + if (kxs.np == null) + throw new IOException("Cannot negotiate, proposals do not match."); + + if (kxs.remoteKEX.isFirst_kex_packet_follows() && (!kxs.np.guessOK)) + { + /* + * Guess was wrong, we need to ignore the next kex packet. + */ + + ignore_next_kex_packet = true; + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) + { + if (kxs.dhgexParameters.getMin_group_len() == 0 || csh.server_versioncomment.matches("OpenSSH_2\\.([0-4]\\.|5\\.[0-2]).*")) + { + PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters); + tm.sendKexMessage(dhgexreq.getPayload()); + } + else + { + PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters); + tm.sendKexMessage(dhgexreq.getPayload()); + } + if (kxs.np.kex_algo.endsWith("sha1")) { + kxs.hashAlgo = "SHA1"; + } else { + kxs.hashAlgo = "SHA-256"; + } + kxs.state = 1; + return; + } + + if (kxs.np.kex_algo.equals(Curve25519Exchange.NAME) + || kxs.np.kex_algo.equals(Curve25519Exchange.ALT_NAME) + || kxs.np.kex_algo.equals("ecdh-sha2-nistp521") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp384") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp256") + || kxs.np.kex_algo.equals("diffie-hellman-group18-sha512") + || kxs.np.kex_algo.equals("diffie-hellman-group16-sha512") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha256") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")) { + kxs.dhx = GenericDhExchange.getInstance(kxs.np.kex_algo); + + kxs.dhx.init(kxs.np.kex_algo); + kxs.hashAlgo = kxs.dhx.getHashAlgo(); + + PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.getE()); + tm.sendKexMessage(kp.getPayload()); + kxs.state = 1; + return; + } + + throw new IllegalStateException("Unknown KEX method!"); + } + + if (msg[0] == Packets.SSH_MSG_NEWKEYS) + { + if (km == null) + throw new IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!"); + + BlockCipher cbc; + MAC mac; + ICompressor comp; + + try + { + cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false, + km.enc_key_server_to_client, km.initial_iv_server_to_client); + + mac = new HMAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client); + + comp = CompressionFactory.createCompressor(kxs.np.comp_algo_server_to_client); + } + catch (IllegalArgumentException e1) + { + throw new IOException("Fatal error during MAC startup: " + e1.getMessage()); + } + + tm.changeRecvCipher(cbc, mac); + tm.changeRecvCompression(comp); + + ConnectionInfo sci = new ConnectionInfo(); + + kexCount++; + + sci.keyExchangeAlgorithm = kxs.np.kex_algo; + sci.keyExchangeCounter = kexCount; + sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server; + sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client; + sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server; + sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client; + sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo; + sci.serverHostKey = kxs.hostkey; + sci.clientToServerCompressionAlgorithm = kxs.np.comp_algo_client_to_server; + sci.serverToClientCompressionAlgorithm = kxs.np.comp_algo_server_to_client; + + synchronized (accessLock) + { + lastConnInfo = sci; + accessLock.notifyAll(); + } + + kxs = null; + return; + } + + if ((kxs == null) || (kxs.state == 0)) + throw new IOException("Unexpected Kex submessage!"); + + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) + { + if (kxs.state == 1) + { + PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen); + kxs.dhgx = new DhGroupExchange(dhgexgrp.getP(), dhgexgrp.getG()); + kxs.dhgx.init(rnd); + PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.getE()); + tm.sendKexMessage(dhgexinit.getPayload()); + kxs.state = 2; + return; + } + + if (kxs.state == 2) + { + PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen); + + kxs.hostkey = dhgexrpl.getHostKey(); + + if (verifier != null) + { + boolean vres = false; + + try + { + vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + } + catch (Exception e) + { + throw new IOException( + "The server hostkey was not accepted by the verifier callback.", e); + } + + if (!vres) + throw new IOException("The server hostkey was not accepted by the verifier callback"); + } + + kxs.dhgx.setF(dhgexrpl.getF()); + + try + { + kxs.H = kxs.dhgx.calculateH(kxs.hashAlgo, + csh.getClientString(), csh.getServerString(), + kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(), + dhgexrpl.getHostKey(), kxs.dhgexParameters); + } + catch (IllegalArgumentException e) + { + throw new IOException("KEX error.", e); + } + + boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey); + + if (!res) + throw new IOException("Hostkey signature sent by remote is wrong!"); + + kxs.K = kxs.dhgx.getK(); + + finishKex(); + kxs.state = -1; + return; + } + + throw new IllegalStateException("Illegal State in KEX Exchange!"); + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha256") + || kxs.np.kex_algo.equals("diffie-hellman-group16-sha512") + || kxs.np.kex_algo.equals("diffie-hellman-group18-sha512") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp256") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp384") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp521") + || kxs.np.kex_algo.equals(Curve25519Exchange.NAME) + || kxs.np.kex_algo.equals(Curve25519Exchange.ALT_NAME)) + { + if (kxs.state == 1) + { + + PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen); + + kxs.hostkey = dhr.getHostKey(); + + if (verifier != null) + { + boolean vres = false; + + try + { + vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + } + catch (Exception e) + { + throw new IOException( + "The server hostkey was not accepted by the verifier callback.", e); + } + + if (!vres) + throw new IOException("The server hostkey was not accepted by the verifier callback"); + } + + kxs.dhx.setF(dhr.getF()); + + try + { + kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(), + kxs.remoteKEX.getPayload(), dhr.getHostKey()); + } + catch (IllegalArgumentException e) + { + throw new IOException("KEX error.", e); + } + + boolean res = verifySignature(dhr.getSignature(), kxs.hostkey); + + if (!res) + throw new IOException("Hostkey signature sent by remote is wrong!"); + + kxs.K = kxs.dhx.getK(); + + finishKex(); + kxs.state = -1; + return; + } + } + + throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")"); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexParameters.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexParameters.java new file mode 100644 index 0000000000..c1de65c59f --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexParameters.java @@ -0,0 +1,24 @@ +package com.trilead.ssh2.transport; + +/** + * KexParameters. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexParameters.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KexParameters +{ + public byte[] cookie; + public String[] kex_algorithms; + public String[] server_host_key_algorithms; + public String[] encryption_algorithms_client_to_server; + public String[] encryption_algorithms_server_to_client; + public String[] mac_algorithms_client_to_server; + public String[] mac_algorithms_server_to_client; + public String[] compression_algorithms_client_to_server; + public String[] compression_algorithms_server_to_client; + public String[] languages_client_to_server; + public String[] languages_server_to_client; + public boolean first_kex_packet_follows; + public int reserved_field1; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexState.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexState.java new file mode 100644 index 0000000000..337d208fe1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/KexState.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.transport; + + +import java.math.BigInteger; + +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.crypto.dh.DhGroupExchange; +import com.trilead.ssh2.crypto.dh.GenericDhExchange; +import com.trilead.ssh2.packets.PacketKexInit; + +/** + * KexState. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexState.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class KexState +{ + public PacketKexInit localKEX; + public PacketKexInit remoteKEX; + public NegotiatedParameters np; + public int state = 0; + + public BigInteger K; + public byte[] H; + + public byte[] hostkey; + + public String hashAlgo; + public GenericDhExchange dhx; + public DhGroupExchange dhgx; + public DHGexParameters dhgexParameters; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/MessageHandler.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/MessageHandler.java new file mode 100644 index 0000000000..f74e19037c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/MessageHandler.java @@ -0,0 +1,14 @@ +package com.trilead.ssh2.transport; + +import java.io.IOException; + +/** + * MessageHandler. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: MessageHandler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public interface MessageHandler +{ + void handleMessage(byte[] msg, int msglen) throws IOException; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiateException.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiateException.java new file mode 100644 index 0000000000..fa99bd99ef --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiateException.java @@ -0,0 +1,12 @@ +package com.trilead.ssh2.transport; + +/** + * NegotiateException. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NegotiateException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class NegotiateException extends Exception +{ + private static final long serialVersionUID = 3689910669428143157L; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java new file mode 100644 index 0000000000..34ed34ad49 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java @@ -0,0 +1,22 @@ +package com.trilead.ssh2.transport; + +/** + * NegotiatedParameters. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NegotiatedParameters.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class NegotiatedParameters +{ + public boolean guessOK; + public String kex_algo; + public String server_host_key_algo; + public String enc_algo_client_to_server; + public String enc_algo_server_to_client; + public String mac_algo_client_to_server; + public String mac_algo_server_to_client; + public String comp_algo_client_to_server; + public String comp_algo_server_to_client; + public String lang_client_to_server; + public String lang_server_to_client; +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportConnection.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportConnection.java new file mode 100644 index 0000000000..19f9cd7ebc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportConnection.java @@ -0,0 +1,364 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; + +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.CipherInputStream; +import com.trilead.ssh2.crypto.cipher.CipherOutputStream; +import com.trilead.ssh2.crypto.cipher.NullCipher; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.Packets; + + +/** + * TransportConnection. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TransportConnection.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class TransportConnection +{ + private static final Logger log = Logger.getLogger(TransportConnection.class); + + int send_seq_number = 0; + + int recv_seq_number = 0; + + CipherInputStream cis; + + CipherOutputStream cos; + + boolean useRandomPadding = false; + + /* Depends on current MAC and CIPHER */ + + MAC send_mac; + + byte[] send_mac_buffer; + + int send_padd_blocksize = 8; + + MAC recv_mac; + + byte[] recv_mac_buffer; + + byte[] recv_mac_buffer_cmp; + + int recv_padd_blocksize = 8; + + ICompressor recv_comp = null; + + ICompressor send_comp = null; + + boolean can_recv_compress = false; + + boolean can_send_compress = false; + + byte[] recv_comp_buffer; + + byte[] send_comp_buffer; + + /* won't change */ + + final byte[] send_padding_buffer = new byte[256]; + + final byte[] send_packet_header_buffer = new byte[5]; + + final byte[] recv_padding_buffer = new byte[256]; + + final byte[] recv_packet_header_buffer = new byte[5]; + + ClientServerHello csh; + + final SecureRandom rnd; + + public TransportConnection(InputStream is, OutputStream os, SecureRandom rnd) + { + this.cis = new CipherInputStream(new NullCipher(), is); + this.cos = new CipherOutputStream(new NullCipher(), os); + this.rnd = rnd; + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + cis.changeCipher(bc); + recv_mac = mac; + recv_mac_buffer = (mac != null) ? new byte[mac.size()] : null; + recv_mac_buffer_cmp = (mac != null) ? new byte[mac.size()] : null; + recv_padd_blocksize = bc.getBlockSize(); + if (recv_padd_blocksize < 8) + recv_padd_blocksize = 8; + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + if (!(bc instanceof NullCipher)) + { + /* Only use zero byte padding for the first few packets */ + useRandomPadding = true; + /* Once we start encrypting, there is no way back */ + } + + cos.changeCipher(bc); + send_mac = mac; + send_mac_buffer = (mac != null) ? new byte[mac.size()] : null; + send_padd_blocksize = bc.getBlockSize(); + if (send_padd_blocksize < 8) + send_padd_blocksize = 8; + } + + public void changeRecvCompression(ICompressor comp) + { + recv_comp = comp; + + if (comp != null) { + recv_comp_buffer = new byte[comp.getBufferSize()]; + can_recv_compress |= recv_comp.canCompressPreauth(); + } + } + + public void changeSendCompression(ICompressor comp) + { + send_comp = comp; + + if (comp != null) { + send_comp_buffer = new byte[comp.getBufferSize()]; + can_send_compress |= send_comp.canCompressPreauth(); + } + } + + public void sendMessage(byte[] message) throws IOException + { + sendMessage(message, 0, message.length, 0); + } + + public void sendMessage(byte[] message, int off, int len) throws IOException + { + sendMessage(message, off, len, 0); + } + + public int getPacketOverheadEstimate() + { + // return an estimate for the paket overhead (for send operations) + return 5 + 4 + (send_padd_blocksize - 1) + send_mac_buffer.length; + } + + public void sendMessage(byte[] message, int off, int len, int padd) throws IOException + { + if (padd < 4) + padd = 4; + else if (padd > 64) + padd = 64; + + if (send_comp != null && can_send_compress) { + if (send_comp_buffer.length < message.length + 1024) + send_comp_buffer = new byte[message.length + 1024]; + len = send_comp.compress(message, off, len, send_comp_buffer); + message = send_comp_buffer; + } + + boolean encryptThenMac = send_mac != null && send_mac.isEncryptThenMac(); + + int encryptedPacketLength = (encryptThenMac ? 1 : 5) + len + padd; /* Minimum allowed padding is 4 */ + + int slack = encryptedPacketLength % send_padd_blocksize; + + if (slack != 0) + { + encryptedPacketLength += (send_padd_blocksize - slack); + } + + if (encryptedPacketLength < 16) + encryptedPacketLength = 16; + + int padd_len = encryptedPacketLength - ((encryptThenMac ? 1 : 5) + len); + + if (useRandomPadding) + { + for (int i = 0; i < padd_len; i = i + 4) + { + /* + * don't waste calls to rnd.nextInt() (by using only 8bit of the + * output). just believe me: even though we may write here up to 3 + * bytes which won't be used, there is no "buffer overflow" (i.e., + * arrayindexoutofbounds). the padding buffer is big enough =) (256 + * bytes, and that is bigger than any current cipher block size + 64). + */ + + int r = rnd.nextInt(); + send_padding_buffer[i] = (byte) r; + send_padding_buffer[i + 1] = (byte) (r >> 8); + send_padding_buffer[i + 2] = (byte) (r >> 16); + send_padding_buffer[i + 3] = (byte) (r >> 24); + } + } + else + { + /* use zero padding for unencrypted traffic */ + for (int i = 0; i < padd_len; i++) + send_padding_buffer[i] = 0; + /* Actually this code is paranoid: we never filled any + * bytes into the padding buffer so far, therefore it should + * consist of zeros only. + */ + } + + int payloadLength = encryptThenMac ? encryptedPacketLength : encryptedPacketLength - 4; + send_packet_header_buffer[0] = (byte) (encryptedPacketLength >> 24); + send_packet_header_buffer[1] = (byte) (payloadLength >> 16); + send_packet_header_buffer[2] = (byte) (payloadLength >> 8); + send_packet_header_buffer[3] = (byte) (payloadLength); + send_packet_header_buffer[4] = (byte) padd_len; + + if (send_mac != null && send_mac.isEncryptThenMac()) { + cos.writePlain(send_packet_header_buffer, 0, 4); + cos.startRecording(); + cos.write(send_packet_header_buffer, 4, 1); + } else { + cos.write(send_packet_header_buffer, 0, 5); + } + cos.write(message, off, len); + cos.write(send_padding_buffer, 0, padd_len); + + if (send_mac != null) + { + send_mac.initMac(send_seq_number); + + if (send_mac.isEncryptThenMac()) { + send_mac.update(send_packet_header_buffer, 0, 4); + byte[] encryptedMessage = cos.getRecordedOutput(); + send_mac.update(encryptedMessage, 0, encryptedMessage.length); + } else { + send_mac.update(send_packet_header_buffer, 0, 5); + send_mac.update(message, off, len); + send_mac.update(send_padding_buffer, 0, padd_len); + } + + send_mac.getMac(send_mac_buffer, 0); + cos.writePlain(send_mac_buffer, 0, send_mac_buffer.length); + } + + cos.flush(); + + if (log.isEnabled()) + { + log.log(90, "Sent " + Packets.getMessageName(message[off] & 0xff) + " " + len + " bytes payload"); + } + + send_seq_number++; + } + + public int receiveMessage(byte[] buffer, int off, int len) throws IOException + { + final int packetLength; + final int payloadLength; + + if (recv_mac != null && recv_mac.isEncryptThenMac()) { + cis.readPlain(recv_packet_header_buffer, 0, 4); + packetLength = getPacketLength(recv_packet_header_buffer, true); + + recv_mac.initMac(recv_seq_number); + recv_mac.update(recv_packet_header_buffer, 0, 4); + + cis.peekPlain(buffer, off, packetLength + recv_mac_buffer.length); + System.arraycopy(buffer, off + packetLength, recv_mac_buffer, 0, recv_mac_buffer.length); + + recv_mac.update(buffer, off, packetLength); + recv_mac.getMac(recv_mac_buffer_cmp, 0); + + checkMacMatches(recv_mac_buffer, recv_mac_buffer_cmp); + + cis.read(recv_packet_header_buffer, 4, 1); + } else { + cis.read(recv_packet_header_buffer, 0, 5); + packetLength = getPacketLength(recv_packet_header_buffer, false); + } + + int paddingLength = recv_packet_header_buffer[4] & 0xff; + + payloadLength = calculatePayloadLength(len, packetLength, paddingLength); + + cis.read(buffer, off, payloadLength); + cis.read(recv_padding_buffer, 0, paddingLength); + + if (recv_mac != null) { + cis.readPlain(recv_mac_buffer, 0, recv_mac_buffer.length); + + if (!recv_mac.isEncryptThenMac()) { + recv_mac.initMac(recv_seq_number); + recv_mac.update(recv_packet_header_buffer, 0, 5); + recv_mac.update(buffer, off, payloadLength); + recv_mac.update(recv_padding_buffer, 0, paddingLength); + recv_mac.getMac(recv_mac_buffer_cmp, 0); + + checkMacMatches(recv_mac_buffer, recv_mac_buffer_cmp); + } + } + + recv_seq_number++; + + if (log.isEnabled()) { + log.log(90, "Received " + Packets.getMessageName(buffer[off] & 0xff) + " " + payloadLength + + " bytes payload"); + } + + if (recv_comp != null && can_recv_compress) { + int[] uncomp_len = new int[] { payloadLength }; + buffer = recv_comp.uncompress(buffer, off, uncomp_len); + + if (buffer == null) { + throw new IOException("Error while inflating remote data"); + } else { + return uncomp_len[0]; + } + } else { + return payloadLength; + } + } + + private static int calculatePayloadLength(int bufferLength, int packetLength, int paddingLength) throws IOException { + int payloadLength = packetLength - paddingLength - 1; + + if (payloadLength < 0) + throw new IOException("Illegal padding_length in packet from remote (" + paddingLength + ")"); + + if (payloadLength >= bufferLength) + throw new IOException("Receive buffer too small (" + bufferLength + ", need " + payloadLength + ")"); + + return payloadLength; + } + + private static void checkMacMatches(byte[] buf1, byte[] buf2) throws IOException { + int difference = 0; + for (int i = 0; i < buf1.length; i++) { + difference |= buf1[i] ^ buf2[i]; + } + if (difference != 0) + throw new IOException("Remote sent corrupt MAC."); + } + + private static int getPacketLength(byte[] packetHeader, boolean isEtm) throws IOException { + int packetLength = ((packetHeader[0] & 0xff) << 24) + | ((packetHeader[1] & 0xff) << 16) | ((packetHeader[2] & 0xff) << 8) + | ((packetHeader[3] & 0xff)); + + if (packetLength > 35000 || packetLength < (isEtm ? 8 : 12)) + throw new IOException("Illegal packet size! (" + packetLength + ")"); + + return packetLength; + } + + /** + * + */ + public void startCompression() { + can_recv_compress = true; + can_send_compress = true; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportManager.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportManager.java new file mode 100644 index 0000000000..ce33608480 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/transport/TransportManager.java @@ -0,0 +1,647 @@ + +package com.trilead.ssh2.transport; + +import com.trilead.ssh2.ExtensionInfo; +import com.trilead.ssh2.packets.PacketExtInfo; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.SecureRandom; +import java.util.Vector; + +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.ProxyData; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketDisconnect; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesReader; + + +/* + * Yes, the "standard" is a big mess. On one side, the say that arbitary channel + * packets are allowed during kex exchange, on the other side we need to blindly + * ignore the next _packet_ if the KEX guess was wrong. Where do we know from that + * the next packet is not a channel data packet? Yes, we could check if it is in + * the KEX range. But the standard says nothing about this. The OpenSSH guys + * block local "normal" traffic during KEX. That's fine - however, they assume + * that the other side is doing the same. During re-key, if they receive traffic + * other than KEX, they become horribly irritated and kill the connection. Since + * we are very likely going to communicate with OpenSSH servers, we have to play + * the same game - even though we could do better. + * + * btw: having stdout and stderr on the same channel, with a shared window, is + * also a VERY good idea... =( + */ + +/** + * TransportManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TransportManager.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TransportManager +{ + private static final Logger log = Logger.getLogger(TransportManager.class); + + class HandlerEntry + { + MessageHandler mh; + int low; + int high; + } + + private final Vector asynchronousQueue = new Vector(); + private Thread asynchronousThread = null; + + class AsynchronousWorker extends Thread + { + public void run() + { + while (true) + { + byte[] msg; + + synchronized (asynchronousQueue) + { + if (asynchronousQueue.size() == 0) + { + /* After the queue is empty for about 2 seconds, stop this thread */ + + try + { + asynchronousQueue.wait(2000); + } + catch (InterruptedException e) + { + /* OKOK, if somebody interrupts us, then we may die earlier. */ + } + + if (asynchronousQueue.size() == 0) + { + asynchronousThread = null; + return; + } + } + + msg = asynchronousQueue.remove(0); + } + + /* The following invocation may throw an IOException. + * There is no point in handling it - it simply means + * that the connection has a problem and we should stop + * sending asynchronously messages. We do not need to signal that + * we have exited (asynchronousThread = null): further + * messages in the queue cannot be sent by this or any + * other thread. + * Other threads will sooner or later (when receiving or + * sending the next message) get the same IOException and + * get to the same conclusion. + */ + + try + { + sendMessage(msg); + } + catch (IOException e) + { + return; + } + } + } + } + + String hostname; + int port; + Socket sock; + + Object connectionSemaphore = new Object(); + + boolean flagKexOngoing = false; + boolean connectionClosed = false; + + Throwable reasonClosedCause = null; + + TransportConnection tc; + KexManager km; + + Vector messageHandlers = new Vector(); + + Thread receiveThread; + + Vector connectionMonitors = new Vector(); + boolean monitorsWereInformed = false; + + private volatile ExtensionInfo extensionInfo = ExtensionInfo.noExtInfoSeen(); + + public TransportManager(String host, int port) { + this.hostname = host; + this.port = port; + } + + public int getPacketOverheadEstimate() + { + return tc.getPacketOverheadEstimate(); + } + + public ConnectionInfo getConnectionInfo(int kexNumber) throws IOException + { + return km.getOrWaitForConnectionInfo(kexNumber); + } + + public ExtensionInfo getExtensionInfo() + { + return extensionInfo; + } + + public Throwable getReasonClosedCause() + { + synchronized (connectionSemaphore) + { + return reasonClosedCause; + } + } + + public byte[] getSessionIdentifier() + { + return km.sessionId; + } + + public void close(Throwable cause, boolean useDisconnectPacket) + { + if (!useDisconnectPacket) + { + /* OK, hard shutdown - do not aquire the semaphore, + * perhaps somebody is inside (and waits until the remote + * side is ready to accept new data). */ + + try + { + if (sock != null) + sock.close(); + } + catch (IOException ignore) + { + } + + /* OK, whoever tried to send data, should now agree that + * there is no point in further waiting =) + * It is safe now to aquire the semaphore. + */ + } + + synchronized (connectionSemaphore) + { + if (!connectionClosed) + { + if (useDisconnectPacket) + { + try + { + byte[] msg = new PacketDisconnect(Packets.SSH_DISCONNECT_BY_APPLICATION, cause.getMessage(), "") + .getPayload(); + if (tc != null) + tc.sendMessage(msg); + } + catch (IOException ignore) + { + } + + try + { + if (sock != null) + sock.close(); + } + catch (IOException ignore) + { + } + } + + connectionClosed = true; + reasonClosedCause = cause; /* may be null */ + } + connectionSemaphore.notifyAll(); + } + + /* No check if we need to inform the monitors */ + + Vector monitors = null; + + synchronized (this) + { + /* Short term lock to protect "connectionMonitors" + * and "monitorsWereInformed" + * (they may be modified concurrently) + */ + + if (!monitorsWereInformed) + { + monitorsWereInformed = true; + monitors = (Vector) connectionMonitors.clone(); + } + } + + if (monitors != null) + { + for (int i = 0; i < monitors.size(); i++) + { + try + { + ConnectionMonitor cmon = (ConnectionMonitor) monitors.elementAt(i); + cmon.connectionLost(reasonClosedCause); + } + catch (Exception ignore) + { + } + } + } + } + + private void establishConnection(ProxyData proxyData, int connectTimeout) throws IOException + { + if (proxyData == null) + sock = connectDirect(hostname, port, connectTimeout); + else + sock = proxyData.openConnection(hostname, port, connectTimeout); + } + + private static Socket connectDirect(String hostname, int port, int connectTimeout) + throws IOException + { + Socket sock = new Socket(); + InetAddress addr = InetAddress.getByName(hostname); + sock.connect(new InetSocketAddress(addr, port), connectTimeout); + sock.setSoTimeout(0); + return sock; + } + + public void initialize(CryptoWishList cwl, ServerHostKeyVerifier verifier, DHGexParameters dhgex, + int connectTimeout, SecureRandom rnd, ProxyData proxyData) throws IOException + { + /* First, establish the TCP connection to the SSH-2 server */ + + establishConnection(proxyData, connectTimeout); + + /* Parse the server line and say hello - important: this information is later needed for the + * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object + * for later use. + */ + + ClientServerHello csh = new ClientServerHello(sock.getInputStream(), sock.getOutputStream()); + + tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd); + + km = new KexManager(this, csh, cwl, hostname, port, verifier, rnd); + km.initiateKEX(cwl, dhgex); + + receiveThread = new Thread(new Runnable() + { + public void run() + { + try + { + receiveLoop(); + } + catch (IOException e) + { + close(e, false); + + if (log.isEnabled()) + log.log(10, "Receive thread: error in receiveLoop: " + e.getMessage()); + } + + if (log.isEnabled()) + log.log(50, "Receive thread: back from receiveLoop"); + + /* Tell all handlers that it is time to say goodbye */ + + if (km != null) + { + try + { + km.handleMessage(null, 0); + } + catch (IOException e) + { + } + } + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + try + { + he.mh.handleMessage(null, 0); + } + catch (Exception ignore) + { + } + } + } + }); + + receiveThread.setDaemon(true); + receiveThread.start(); + } + + public void registerMessageHandler(MessageHandler mh, int low, int high) + { + HandlerEntry he = new HandlerEntry(); + he.mh = mh; + he.low = low; + he.high = high; + + synchronized (messageHandlers) + { + messageHandlers.addElement(he); + } + } + + public void removeMessageHandler(MessageHandler mh, int low, int high) + { + synchronized (messageHandlers) + { + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + if ((he.mh == mh) && (he.low == low) && (he.high == high)) + { + messageHandlers.removeElementAt(i); + break; + } + } + } + } + + public void sendKexMessage(byte[] msg) throws IOException + { + synchronized (connectionSemaphore) + { + if (connectionClosed) + { + throw new IOException("Sorry, this connection is closed.", reasonClosedCause); + } + + flagKexOngoing = true; + + try + { + tc.sendMessage(msg); + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + + public void kexFinished() { + synchronized (connectionSemaphore) + { + flagKexOngoing = false; + connectionSemaphore.notifyAll(); + } + } + + public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex) throws IOException + { + km.initiateKEX(cwl, dhgex); + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + tc.changeRecvCipher(bc, mac); + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + tc.changeSendCipher(bc, mac); + } + + /** + * @param comp + */ + public void changeRecvCompression(ICompressor comp) { + tc.changeRecvCompression(comp); + } + + /** + * @param comp + */ + public void changeSendCompression(ICompressor comp) { + tc.changeSendCompression(comp); + } + + /** + * + */ + public void startCompression() { + tc.startCompression(); + } + + public void sendAsynchronousMessage(byte[] msg) throws IOException + { + synchronized (asynchronousQueue) + { + asynchronousQueue.addElement(msg); + + /* This limit should be flexible enough. We need this, otherwise the peer + * can flood us with global requests (and other stuff where we have to reply + * with an asynchronous message) and (if the server just sends data and does not + * read what we send) this will probably put us in a low memory situation + * (our send queue would grow and grow and...) */ + + if (asynchronousQueue.size() > 100) + throw new IOException("Error: the peer is not consuming our asynchronous replies."); + + /* Check if we have an asynchronous sending thread */ + + if (asynchronousThread == null) + { + asynchronousThread = new AsynchronousWorker(); + asynchronousThread.setDaemon(true); + asynchronousThread.start(); + + /* The thread will stop after 2 seconds of inactivity (i.e., empty queue) */ + } + } + } + + public void setConnectionMonitors(Vector monitors) + { + synchronized (this) + { + connectionMonitors = (Vector) monitors.clone(); + } + } + + public void sendMessage(byte[] msg) throws IOException + { + if (Thread.currentThread() == receiveThread) + throw new IOException("Assertion error: sendMessage may never be invoked by the receiver thread!"); + + synchronized (connectionSemaphore) + { + while (true) + { + if (connectionClosed) + { + throw new IOException("Sorry, this connection is closed.", reasonClosedCause); + } + + if (!flagKexOngoing) + break; + + try + { + connectionSemaphore.wait(); + } + catch (InterruptedException e) + { + } + } + + try + { + tc.sendMessage(msg); + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + + public void receiveLoop() throws IOException + { + byte[] msg = new byte[35004]; + + while (true) + { + int msglen = tc.receiveMessage(msg, 0, msg.length); + + int type = msg[0] & 0xff; + + if (type == Packets.SSH_MSG_IGNORE) + continue; + + if (type == Packets.SSH_MSG_DEBUG) + { + if (log.isEnabled()) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + tr.readBoolean(); + StringBuffer debugMessageBuffer = new StringBuffer(); + debugMessageBuffer.append(tr.readString("UTF-8")); + + for (int i = 0; i < debugMessageBuffer.length(); i++) + { + char c = debugMessageBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + debugMessageBuffer.setCharAt(i, '\uFFFD'); + } + + log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'"); + } + continue; + } + + if (type == Packets.SSH_MSG_UNIMPLEMENTED) + { + throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen."); + } + + if (type == Packets.SSH_MSG_DISCONNECT) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + int reason_code = tr.readUINT32(); + StringBuffer reasonBuffer = new StringBuffer(); + reasonBuffer.append(tr.readString("UTF-8")); + + /* + * Do not get fooled by servers that send abnormal long error + * messages + */ + + if (reasonBuffer.length() > 255) + { + reasonBuffer.setLength(255); + reasonBuffer.setCharAt(254, '.'); + reasonBuffer.setCharAt(253, '.'); + reasonBuffer.setCharAt(252, '.'); + } + + /* + * Also, check that the server did not send charcaters that may + * screw up the receiver -> restrict to reasonable US-ASCII + * subset -> "printable characters" (ASCII 32 - 126). Replace + * all others with 0xFFFD (UNICODE replacement character). + */ + + for (int i = 0; i < reasonBuffer.length(); i++) + { + char c = reasonBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + reasonBuffer.setCharAt(i, '\uFFFD'); + } + + throw new IOException("Peer sent DISCONNECT message (reason code " + reason_code + "): " + + reasonBuffer.toString()); + } + + /* + * Is it a KEX Packet? + */ + + if ((type == Packets.SSH_MSG_KEXINIT) || (type == Packets.SSH_MSG_NEWKEYS) + || ((type >= 30) && (type <= 49))) + { + km.handleMessage(msg, msglen); + continue; + } + + if (type == Packets.SSH_MSG_USERAUTH_SUCCESS) { + tc.startCompression(); + } + + if (type == Packets.SSH_MSG_EXT_INFO) { + // Update most-recently seen ext info (server can send this multiple times) + extensionInfo = ExtensionInfo.fromPacketExtInfo( + new PacketExtInfo(msg, 0, msglen)); + continue; + } + + MessageHandler mh = null; + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + if ((he.low <= type) && (type <= he.high)) + { + mh = he.mh; + break; + } + } + + if (mh == null) + throw new IOException("Unexpected SSH message (type " + type + ")"); + + mh.handleMessage(msg, msglen); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/TimeoutService.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/TimeoutService.java new file mode 100644 index 0000000000..27bc8433c9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/TimeoutService.java @@ -0,0 +1,149 @@ + +package com.trilead.ssh2.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collections; +import java.util.LinkedList; + +import com.trilead.ssh2.log.Logger; + + +/** + * TimeoutService (beta). Here you can register a timeout. + *

+ * Implemented having large scale programs in mind: if you open many concurrent SSH connections + * that rely on timeouts, then there will be only one timeout thread. Once all timeouts + * have expired/are cancelled, the thread will (sooner or later) exit. + * Only after new timeouts arrive a new thread (singleton) will be instantiated. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TimeoutService.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class TimeoutService +{ + private static final Logger log = Logger.getLogger(TimeoutService.class); + + public static class TimeoutToken implements Comparable + { + private long runTime; + private Runnable handler; + + private TimeoutToken(long runTime, Runnable handler) + { + this.runTime = runTime; + this.handler = handler; + } + + public int compareTo(Object o) + { + TimeoutToken t = (TimeoutToken) o; + if (runTime > t.runTime) + return 1; + if (runTime == t.runTime) + return 0; + return -1; + } + } + + private static class TimeoutThread extends Thread + { + public void run() + { + synchronized (todolist) + { + while (true) + { + if (todolist.size() == 0) + { + timeoutThread = null; + return; + } + + long now = System.currentTimeMillis(); + + TimeoutToken tt = (TimeoutToken) todolist.getFirst(); + + if (tt.runTime > now) + { + /* Not ready yet, sleep a little bit */ + + try + { + todolist.wait(tt.runTime - now); + } + catch (InterruptedException e) + { + } + + /* We cannot simply go on, since it could be that the token + * was removed (cancelled) or another one has been inserted in + * the meantime. + */ + + continue; + } + + todolist.removeFirst(); + + try + { + tt.handler.run(); + } + catch (Exception e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + log.log(20, "Exeception in Timeout handler:" + e.getMessage() + "(" + sw.toString() + ")"); + } + } + } + } + } + + /* The list object is also used for locking purposes */ + private static final LinkedList todolist = new LinkedList(); + + private static Thread timeoutThread = null; + + /** + * It is assumed that the passed handler will not execute for a long time. + * + * @param runTime + * @param handler + * @return a TimeoutToken that can be used to cancel the timeout. + */ + public static final TimeoutToken addTimeoutHandler(long runTime, Runnable handler) + { + TimeoutToken token = new TimeoutToken(runTime, handler); + + synchronized (todolist) + { + todolist.add(token); + Collections.sort(todolist); + + if (timeoutThread != null) + timeoutThread.interrupt(); + else + { + timeoutThread = new TimeoutThread(); + timeoutThread.setDaemon(true); + timeoutThread.start(); + } + } + + return token; + } + + public static final void cancelTimeoutHandler(TimeoutToken token) + { + synchronized (todolist) + { + todolist.remove(token); + + if (timeoutThread != null) + timeoutThread.interrupt(); + } + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/Tokenizer.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/Tokenizer.java new file mode 100644 index 0000000000..753477d1c5 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/trilead/ssh2/util/Tokenizer.java @@ -0,0 +1,49 @@ + +package com.trilead.ssh2.util; + +/** + * Tokenizer. Why? Because StringTokenizer is not available in J2ME. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Tokenizer.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class Tokenizer +{ + /** + * Exists because StringTokenizer is not available in J2ME. + * Returns an array with at least 1 entry. + * + * @param source must be non-null + * @param delimiter + * @return an array of Strings + */ + public static String[] parseTokens(String source, char delimiter) + { + if (source.length() == 0) + return new String[0]; + + int numtoken = 1; + + for (int i = 0; i < source.length(); i++) { + if (source.charAt(i) == delimiter) + numtoken++; + } + + String[] list = new String[numtoken]; + int nextfield = 0; + + for (int i = 0; i < numtoken; i++) { + if (nextfield >= source.length()) { + list[i] = ""; + } else { + int idx = source.indexOf(delimiter, nextfield); + if (idx == -1) + idx = source.length(); + list[i] = source.substring(nextfield, idx); + nextfield = idx + 1; + } + } + + return list; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseActivity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseActivity.java new file mode 100644 index 0000000000..7efb752be3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseActivity.java @@ -0,0 +1,285 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.base; + +import android.Manifest; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.widget.Toast; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import com.mogo.eagle.core.utilcode.util.ThreadUtils; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.PermissionUtil; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.ToastUtil; +import com.zhjt.mogo_core_function_devatools.rviz.dialog.CommonLoadingDialog; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Set; + +public abstract class BaseActivity extends AppCompatActivity { + private CommonLoadingDialog mLoadingDialog; + private BaseHandler mBaseHandler; + private boolean isFront = false; + private ActivityResultLauncher intentActivityResultLauncher; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initRegisterForActivityResult(); + checkSavePermission(); + canDrawOverlays(); + } + + private void initRegisterForActivityResult() { + intentActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult result) { + checkSavePermission(); +// Intent data = result.getData(); +// int resultCode = result.getResultCode(); + //RESULT_OK +// Log.i("dddd", "resultCode=" + resultCode); + } + }); + } + + + // 跳转到当前应用的设置界面 + private void goToAppSetting() { + Uri uri = Uri.fromParts("package", getPackageName(), null); + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(uri); + intentActivityResultLauncher.launch(intent); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + PermissionUtil.onRequestPermissionsResult(this, permissions, grantResults, permissionsListener, false); + } + + //权限申请回调 + private final PermissionUtil.OnPermissionsListener permissionsListener = new PermissionUtil.OnPermissionsListener() { + @Override + public void onPermissionsOwned() { + + } + + @Override + public void onPermissionsForbidden(String[] permissions, int[] grantResults, ArrayList pmList) { + Set nameSet = PermissionUtil.getPermissionsNameByChinese(pmList.toArray(new String[0])); + if (nameSet != null && nameSet.size() > 0) { + AlertDialog dialog = new AlertDialog.Builder(BaseActivity.this).setTitle("警告").setMessage("请前往设置中手动授予" + nameSet.toString() + "权限,否则功能无法正常运行!").setNegativeButton("返回", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }).setPositiveButton("去设置", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + goToAppSetting(); + } + }).create(); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + } + + @Override + public void onPermissionsDenied(String[] permissions, int[] grantResults, ArrayList pmList) { + Set nameSet = PermissionUtil.getPermissionsNameByChinese(pmList.toArray(new String[0])); + if (nameSet != null && nameSet.size() > 0) { + //重新请求权限 + AlertDialog dialog = new AlertDialog.Builder(BaseActivity.this).setTitle("提示").setMessage(nameSet.toString() + "权限为应用必要权限,请授权").setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String[] sList = pmList.toArray(new String[0]); + //重新申请权限,通过权限名的方式申请多组权限 + PermissionUtil.requestByPermissionName(BaseActivity.this, sList, 10000, permissionsListener); + } + }).create(); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + } + + @Override + public void onPermissionsSucceed() { + } + }; + + /** + * 权限检查 + */ + private void checkSavePermission() { + //权限申请 + String[] pgList = new String[]{Manifest.permission_group.STORAGE}; + PermissionUtil.requestByGroupName(this, pgList, 10000, permissionsListener); + } + + + /** + * 跳转浮层权限获取页面 + */ + private void canDrawOverlays() { + if (!Settings.canDrawOverlays(this)) { + showToastCenter("当前无悬浮窗权限,请授权"); + startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()))); + } + } + + protected void showToastCenter(String msg) { + showToastCenter(msg, Toast.LENGTH_SHORT); + } + + protected void showToastCenter(String msg, int duration) { + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtil.showToastCenter(BaseActivity.this, msg, duration); + } + }); + } + + /** + * 显示加载对话框 + */ + protected void showLoadingDialog() { + showLoadingDialog(null); + } + + protected void showLoadingDialog(String msg) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (isFront) { + if (mLoadingDialog == null) { + mLoadingDialog = new CommonLoadingDialog(android.R.color.black, msg); + } + if (!mLoadingDialog.isAdded()) { + mLoadingDialog.setCancelable(false); + mLoadingDialog.show(getSupportFragmentManager(), "LoadingDialog"); + } else { + mLoadingDialog.setMsg(msg); + } + } + } + }); + } + + /** + * 关闭加载对话框 + */ + public void dismissLoadingDialog() { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mLoadingDialog != null) { + if (mLoadingDialog.isAdded()) { + mLoadingDialog.dismissAllowingStateLoss(); + } + mLoadingDialog = null; //将 LoadingDialog 设置为空,释放强引用 + } + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + isFront = true; + } + + @Override + protected void onPause() { + super.onPause(); + isFront = false; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + ToastUtil.destroyToast(); + dismissLoadingDialog(); + if (getHandler() != null) getHandler().removeCallbacksAndMessages(null); + } + + /** + * 初始化一个Handler,如果需要使用Handler,先调用此方法, + * 然后可以使用postRunnable(Runnable runnable), + * sendMessage在handleMessage(Message msg)中接收msg + */ + public void initHandler() { + mBaseHandler = new BaseHandler(this); + } + + /** + * 返回Handler,在此之前确定已经调用initHandler() + * + * @return Handler + */ + public Handler getHandler() { + return mBaseHandler; + } + + + /** + * 同Handler 的 handleMessage, + * getHandler.sendMessage,发送的Message在此接收 + * 在此之前确定已经调用initHandler() + * + * @param msg + */ + protected void handleMessage(Message msg) { + + } + + /** + * 同Handler的postRunnable + * 在此之前确定已经调用initHandler() + */ + protected void postRunnable(Runnable runnable) { + postRunnableDelayed(runnable, 0); + } + + /** + * 同Handler的postRunnableDelayed + * 在此之前确定已经调用initHandler() + */ + protected void postRunnableDelayed(Runnable runnable, long delayMillis) { + if (mBaseHandler == null) initHandler(); + mBaseHandler.postDelayed(runnable, delayMillis); + } + + + protected static class BaseHandler extends Handler { + private final WeakReference mObjects; + + public BaseHandler(BaseActivity mPresenter) { + mObjects = new WeakReference(mPresenter); + } + + @Override + public void handleMessage(Message msg) { + BaseActivity mPresenter = mObjects.get(); + if (mPresenter != null) mPresenter.handleMessage(msg); + } + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseAdapter.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseAdapter.java new file mode 100644 index 0000000000..b8b9b9f8f6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseAdapter.java @@ -0,0 +1,115 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.base; + + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + + +/** + * RecycleView Adapter + * Created by renfeicui on 2018/10/12. + */ +public abstract class BaseAdapter extends RecyclerView.Adapter { + protected String TAG = this.getClass().getSimpleName(); + protected List mDatas; + protected Context mContext; + private OnItemClickListener mItemClick; + + public interface OnItemClickListener { + void onItemClick(int position, D data); + } + + + public BaseAdapter() { + } + + public BaseAdapter(List mDatas) { + this.mDatas = mDatas; + } + + public BaseAdapter(OnItemClickListener listener) { + mItemClick = listener; + } + + public BaseAdapter(List mDatas, OnItemClickListener listener) { + this.mDatas = mDatas; + mItemClick = listener; + } + + public void setData(List mDatas) { + this.mDatas = mDatas; + if (!mDatas.isEmpty()) + notifyDataSetChanged(); + } + + public List getData() { + return mDatas; + } + + public void setOnItemClickListener(OnItemClickListener listener) { + mItemClick = listener; + } + + /*** + * 获取制定 位置的Data + * @param position 下标 + * @return Data + */ + public D getItem(int position) { + return mDatas == null ? null : mDatas.get(position); + } + + @Override + public int getItemCount() { + return mDatas == null ? 0 : mDatas.size(); + } + + @Override + public void onBindViewHolder(@NonNull VH viewHolder, int position) { + D bean = getItem(position); + onBindDataToItem(viewHolder, bean, position); + } + + + @NonNull + @Override + public VH onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + mContext = viewGroup.getContext(); + return getViewHolder(getItemViewResource(viewGroup)); + } + + /*** + * 同onBindViewHolder() + * @param viewHolder viewHolder + * @param data 数据 + * @param position 下标 + */ + protected abstract void onBindDataToItem(VH viewHolder, D data, int position); + + /*** + * 获取Item布局 + * @return id + */ + protected abstract View getItemViewResource(ViewGroup viewGroup); + + /** + * 获取ViewHolder + * + * @param view + * @return + */ + protected abstract VH getViewHolder(View view); + + public void onClick(BaseViewHolder viewHolder) { + if (mItemClick != null) { + mItemClick.onItemClick(viewHolder.getAdapterPosition(), getItem(viewHolder.getAdapterPosition())); + } + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseDialog.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseDialog.java new file mode 100644 index 0000000000..d76ec14ea3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseDialog.java @@ -0,0 +1,172 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.base; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.View; + +import java.lang.ref.WeakReference; + + +/** + * Created by xfk on 2016/11/2. + */ +public abstract class BaseDialog extends Dialog { + protected final String TAG = this.getClass().getSimpleName(); + protected View rootView; + private BaseHandler mBaseHandler; + + public BaseDialog(Context context) { +// super(context, R.style.CustomDialog); + super(context); + } + + public BaseDialog(Context context, int style) { + super(context, style); + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + onViewCreateBefore(); + onSetContentView(); + onViewInitBefore(); + onViewInit(); + onViewCreated(); + setOnListener(); + setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + BaseDialog.this.onDismiss(); + } + }); + } + + + /** + * called by {@link # onCreate} + * 在 setContentView 方法前调用 + */ + protected void onViewCreateBefore() { + } + + + /** + * 初始化ContentView后调用,View初始化之前,setContentView之后 + */ + protected void onViewInitBefore() { + + } + + /** + * 初始化ContentView后调用,可以进行findViewById等操作onViewInit + */ + protected void onViewInit() { + } + + /** + * called by {@link # onCreate} + * 在 setContentView 方法后调用 + */ + protected void onViewCreated() { + } + + + /** + * setContentView + */ + protected void onSetContentView() { + rootView = getContentViewResource(); + setContentView(rootView); + } + + + /** + * called by {@link # onCreate} + * 进行设置监听 + */ + protected void setOnListener() { + } + + /** + * 得到 ContentView 的 Resource + * + * @return eg R.layout.main_layout + */ + protected abstract View getContentViewResource(); + + + protected void onDismiss() { + if (getHandler() != null) { + getHandler().removeCallbacksAndMessages(null); + } + } + + + /** + * 初始化一个Handler,如果需要使用Handler,先调用此方法, + * 然后可以使用postRunnable(Runnable runnable), + * sendMessage在handleMessage(Message msg)中接收msg + */ + public void initHandler() { + mBaseHandler = new BaseHandler(this); + } + + /** + * 返回Handler,在此之前确定已经调用initHandler() + * + * @return Handler + */ + public Handler getHandler() { + return mBaseHandler; + } + + /** + * 同Handler的postRunnable + * 在此之前确定已经调用initHandler() + */ + protected void postRunnable(Runnable runnable) { + postRunnableDelayed(runnable, 0); + } + + /** + * 同Handler的postRunnableDelayed + * 在此之前确定已经调用initHandler() + */ + protected void postRunnableDelayed(Runnable runnable, long delayMillis) { + if (mBaseHandler == null) initHandler(); + mBaseHandler.postDelayed(runnable, delayMillis); + } + + + /** + * 同Handler 的 handleMessage, + * getHandler.sendMessage,发送的Message在此接收 + * 在此之前确定已经调用initHandler() + * + * @param msg + */ + protected void handleMessage(Message msg) { + } + + + protected static class BaseHandler extends Handler { + private final WeakReference mObjects; + + public BaseHandler(BaseDialog mPresenter) { + mObjects = new WeakReference(mPresenter); + } + + @Override + public void handleMessage(Message msg) { + BaseDialog mPresenter = mObjects.get(); + if (mPresenter != null) mPresenter.handleMessage(msg); + } + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseFragment.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseFragment.java new file mode 100644 index 0000000000..496358535a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseFragment.java @@ -0,0 +1,102 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.base; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.lang.ref.WeakReference; + + +/** + * @author song kenan + * @des + * @date 2021/8/16 + */ +public abstract class BaseFragment extends Fragment { + protected final String TAG = this.getClass().getSimpleName(); + private BaseHandler mBaseHandler; + protected View view; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + view = getContentViewResource(inflater, container); + return view; + } + + /** + * 得到 ContentView 的 Resource + * + * @return eg R.layout.main_layout + */ + public abstract View getContentViewResource(@NonNull LayoutInflater inflater, @Nullable ViewGroup container); + + /** + * 初始化一个Handler,如果需要使用Handler,先调用此方法, + * 然后可以使用postRunnable(Runnable runnable), + * sendMessage在handleMessage(Message msg)中接收msg + */ + public void initHandler() { + mBaseHandler = new BaseHandler(this); + } + + /** + * 返回Handler,在此之前确定已经调用initHandler() + * + * @return Handler + */ + public Handler getHandler() { + return mBaseHandler; + } + + + /** + * 同Handler 的 handleMessage, + * getHandler.sendMessage,发送的Message在此接收 + * 在此之前确定已经调用initHandler() + * + * @param msg + */ + protected void handleMessage(Message msg) { + + } + + /** + * 同Handler的postRunnable + * 在此之前确定已经调用initHandler() + */ + protected void postRunnable(Runnable runnable) { + postRunnableDelayed(runnable, 0); + } + + /** + * 同Handler的postRunnableDelayed + * 在此之前确定已经调用initHandler() + */ + protected void postRunnableDelayed(Runnable runnable, long delayMillis) { + if (mBaseHandler == null) initHandler(); + mBaseHandler.postDelayed(runnable, delayMillis); + } + + + protected static class BaseHandler extends Handler { + private final WeakReference mObjects; + + public BaseHandler(BaseFragment mPresenter) { + mObjects = new WeakReference(mPresenter); + } + + @Override + public void handleMessage(Message msg) { + BaseFragment mPresenter = mObjects.get(); + if (mPresenter != null) + mPresenter.handleMessage(msg); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseService.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseService.java new file mode 100644 index 0000000000..eec9f1061e --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseService.java @@ -0,0 +1,93 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.base; + +import android.app.Service; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.lang.ref.WeakReference; + +public abstract class BaseService extends Service { + private BaseHandler mBaseHandler; + + /** + * 初始化一个Handler,如果需要使用Handler,先调用此方法, + * 然后可以使用postRunnable(Runnable runnable), + * sendMessage在handleMessage(Message msg)中接收msg + */ + protected void initHandler() { + initHandler(true); + } + + protected void initHandler(boolean isMain) { + if (isMain) { + mBaseHandler = new BaseHandler(this, Looper.getMainLooper()); + } else { + new Thread() { + @Override + public void run() { + super.run(); + Looper.prepare(); // 创建Looper + mBaseHandler = new BaseHandler(BaseService.this, Looper.myLooper()); + Looper.loop(); // 启动Looper的消息循环 + } + }.start(); + } + + } + + /** + * 返回Handler,在此之前确定已经调用initHandler() + * + * @return Handler + */ + protected Handler getHandler() { + return mBaseHandler; + } + + + /** + * 同Handler 的 handleMessage, + * getHandler.sendMessage,发送的Message在此接收 + * 在此之前确定已经调用initHandler() + * + * @param msg + */ + protected void handleMessage(Message msg) { + + } + + /** + * 同Handler的postRunnable + * 在此之前确定已经调用initHandler() + */ + protected void postRunnable(Runnable runnable) { + postRunnableDelayed(runnable, 0); + } + + /** + * 同Handler的postRunnableDelayed + * 在此之前确定已经调用initHandler() + */ + protected void postRunnableDelayed(Runnable runnable, long delayMillis) { + if (mBaseHandler == null) initHandler(); + mBaseHandler.postDelayed(runnable, delayMillis); + } + + + protected static class BaseHandler extends Handler { + private final WeakReference mObjects; + + public BaseHandler(BaseService mPresenter, Looper looper) { + super(looper); + mObjects = new WeakReference(mPresenter); + } + + @Override + public void handleMessage(Message msg) { + BaseService mPresenter = mObjects.get(); + if (mPresenter != null) mPresenter.handleMessage(msg); + } + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseViewHolder.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseViewHolder.java new file mode 100644 index 0000000000..ee4248ff5b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/base/BaseViewHolder.java @@ -0,0 +1,26 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.base; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + + +public abstract class BaseViewHolder extends RecyclerView.ViewHolder { + private T adapter; + public View itemView; + + public BaseViewHolder(View itemView, final T adapter) { + super(itemView); + this.itemView = itemView; + this.adapter = adapter; + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + adapter.onClick(BaseViewHolder.this); + } + }); + + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/config/SSHAccountConfig.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/config/SSHAccountConfig.kt new file mode 100644 index 0000000000..1a94482508 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/config/SSHAccountConfig.kt @@ -0,0 +1,94 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.config + +import com.zhidao.support.adas.high.common.MMKVUtils +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.AESUtil + +/** + * @author XuXinChao + * @description 持久化保存账号用户名、密码 + * @since: 2022/8/26 + */ +object SSHAccountConfig { + + private const val USER_NAME = "ros_user_name" + private const val PASSWORD = "ros_password" + private const val ROS_MASTER_IP = "ros_master_ip_address" + + private var ssh_ip = "4JbUOUFpOo2eKdhLmFnfog==" + private var ssh_user_name = "DmIfr6unQRyOkNg7//DWgQ==" + private var ssh_password = "bXSUgtmB0mZd2mj6gaT71g==" + + + private val mmkvUtils = MMKVUtils.getInstance() + + /** + * 获取用户名 + * @return 用户名 + */ + fun getUserName(): String { + return AESUtil.decryptAES( + mmkvUtils.getString( + USER_NAME, + ssh_user_name + ) + ) + } + + /** + * 设置用户名 + * @param userName 用户名 + */ + fun setUserName(userName: String) { + mmkvUtils.put(USER_NAME, AESUtil.encryptAES(userName)) + } + + fun removeUserName() { + MMKVUtils.getInstance().removeKey(USER_NAME) + } + + /** + * 获取用户密码 + * @return 用户密码 + */ + fun getPassWord(): String { + return AESUtil.decryptAES( + mmkvUtils.getString( + PASSWORD, + ssh_password + ) + ) + } + + /** + * 设置用户密码 + * @param password 用户密码 + */ + fun setPassWord(password: String) { + mmkvUtils.put(PASSWORD, AESUtil.encryptAES(password)) + } + + fun removePassWord() { + MMKVUtils.getInstance().removeKey(PASSWORD) + } + + /** + * 设置主控制器IP + * @param rosMasterIp 主控制器IP + */ + fun setRosMasterIp(rosMasterIp: String) { + mmkvUtils.put(ROS_MASTER_IP, AESUtil.encryptAES(rosMasterIp)) + } + + + /** + * 获取主控制器IP + * @return 主控制器IP + */ + fun getRosMasterIp(): String { + return AESUtil.decryptAES(mmkvUtils.getString(ROS_MASTER_IP, ssh_ip)) + } + + fun removeRosMasterIp() { + MMKVUtils.getInstance().removeKey(ROS_MASTER_IP) + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/coroutines/FlowBus.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/coroutines/FlowBus.kt new file mode 100644 index 0000000000..7f150b54a7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/coroutines/FlowBus.kt @@ -0,0 +1,129 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.coroutines + +import android.util.Log +import androidx.lifecycle.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +/** + * FlowBus消息总线 + */ +object FlowBus { + private const val TAG = "FlowBus" + private val busMap = mutableMapOf>() + private val busStickMap = mutableMapOf>() + + @Synchronized + fun with(key: String): EventBus { + var eventBus = busMap[key] + if (eventBus == null) { + eventBus = EventBus(key) + busMap[key] = eventBus + } + return eventBus as EventBus + } + + @Synchronized + fun withStick(key: String): StickEventBus { + var eventBus = busStickMap[key] + if (eventBus == null) { + eventBus = StickEventBus(key) + busStickMap[key] = eventBus + } + return eventBus as StickEventBus + } + + //真正实现类 + open class EventBus(private val key: String) : LifecycleObserver { + + //私有对象用于发送消息 + private val _events: MutableSharedFlow by lazy { + obtainEvent() + } + + //暴露的公有对象用于接收消息 + val events = _events.asSharedFlow() + + //LifecycleOwner和Observer对象集合 + private val mLifecycleOwnerMap = mutableMapOf() + private val mObserverMap = mutableMapOf>() + + open fun obtainEvent(): MutableSharedFlow = + MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST) + + //主线程接收数据 + fun register(lifecycleOwner: LifecycleOwner, observer: Observer) { + Log.d(TAG, "key==$key lifecycleOwner - :$lifecycleOwner observer=$observer") + val tag = key+lifecycleOwner + mLifecycleOwnerMap[tag] = lifecycleOwner + mObserverMap[tag] = observer + mLifecycleOwnerMap[tag]!!.lifecycle.addObserver(this) + mLifecycleOwnerMap.forEach { + val key = it.key + it.value.lifecycleScope.launch { + events.collect{ event-> + try { + mObserverMap[key]?.onChanged(event) + }catch (e: Exception){ + e.printStackTrace() + Log.e(TAG, "FlowBus - Error:$e") + } + + } + } + } + } + + fun unRegister(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.cancel() + } + + //协程中发送数据 + suspend fun post(event: T) { + _events.emit(event) + } + + //主线程发送数据 + fun post(scope: CoroutineScope, event: T) { + scope.launch { + _events.emit(event) + } + } + + //自动销毁 + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun onDestroy() { + Log.w(TAG, "FlowBus - 自动onDestroy - key=$key") + // 移除监听 + val lifeIterator = mLifecycleOwnerMap.iterator() + while(lifeIterator.hasNext()){ + val entry = lifeIterator.next() + if(entry.key.contains(key)){ + entry.value.lifecycle.removeObserver(this) + entry.value.lifecycleScope.cancel() + lifeIterator.remove() + } + } + val observerIterator = mObserverMap.iterator() + while(observerIterator.hasNext()){ + val observer = observerIterator.next() + if(observer.key.contains(key)){ + observerIterator.remove() + } + } + val subscriptCount = _events.subscriptionCount.value + if (subscriptCount <= 0) + busMap.remove(key) + } + } + + class StickEventBus(key: String) : EventBus(key) { + override fun obtainEvent(): MutableSharedFlow = + MutableSharedFlow(replay = 1, extraBufferCapacity = 16, BufferOverflow.DROP_OLDEST) + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/db/BaseDao.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/db/BaseDao.java new file mode 100644 index 0000000000..6b70a20036 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/db/BaseDao.java @@ -0,0 +1,21 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.db; + +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Update; + +/** + * 数据库操作接口 + */ +public interface BaseDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + long insert(T obj); + + @Update(onConflict = OnConflictStrategy.REPLACE) + void update(T obj); + + @Delete + int delete(T obj); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/AESUtil.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/AESUtil.java new file mode 100644 index 0000000000..c1391d71f1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/AESUtil.java @@ -0,0 +1,53 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils; + +import android.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +public class AESUtil { + + // AES密钥,16字节、24字节或32字节 + private static final String AES_KEY = "1234567890123456"; + + /** + * 加密 + * @param data + * @return + */ + public static String encryptAES(String data) { + try { + Cipher cipher = Cipher.getInstance("AES"); + byte[] keyBytes = new byte[16]; + System.arraycopy(AES_KEY.getBytes(), 0, keyBytes, 0, Math.min(AES_KEY.getBytes().length, keyBytes.length)); + SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + byte[] encryptedBytes = cipher.doFinal(data.getBytes()); + return Base64.encodeToString(encryptedBytes, Base64.DEFAULT); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 解密 + * @param encryptedData + * @return + */ + public static String decryptAES(String encryptedData) { + try { + Cipher cipher = Cipher.getInstance("AES"); + byte[] keyBytes = new byte[16]; + System.arraycopy(AES_KEY.getBytes(), 0, keyBytes, 0, Math.min(AES_KEY.getBytes().length, keyBytes.length)); + SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + byte[] decryptedBytes = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT)); + return new String(decryptedBytes); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/DetectHtml.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/DetectHtml.java new file mode 100644 index 0000000000..dd878a3f46 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/DetectHtml.java @@ -0,0 +1,41 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils; + +/** + * Detect HTML markup in a string + * This will detect tags or entities + * + * @author dbennett455@gmail.com - David H. Bennett + */ + +import java.util.regex.Pattern; + +public class DetectHtml { + // adapted from post by Phil Haack and modified to match better + public final static String tagStart = + "\\<\\w+((\\s+\\w+(\\s*\\=\\s*(?:\".*?\"|'.*?'|[^'\"\\>\\s]+))?)+\\s*|\\s*)\\>"; + public final static String tagEnd = + "\\"; + public final static String tagSelfClosing = + "\\<\\w+((\\s+\\w+(\\s*\\=\\s*(?:\".*?\"|'.*?'|[^'\"\\>\\s]+))?)+\\s*|\\s*)/\\>"; + public final static String htmlEntity = + "&[a-zA-Z][a-zA-Z0-9]+;"; + public final static Pattern htmlPattern = Pattern.compile( + "(" + tagStart + ".*" + tagEnd + ")|(" + tagSelfClosing + ")|(" + htmlEntity + ")", + Pattern.DOTALL + ); + + /** + * Will return true if s contains HTML markup tags or entities. + * + * @param s String to test + * @return true if string contains HTML + */ + public static boolean isHtml(String s) { + boolean ret = false; + if (s != null) { + ret = htmlPattern.matcher(s).find(); + } + return ret; + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/LambdaTask.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/LambdaTask.java new file mode 100644 index 0000000000..94852c0a06 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/LambdaTask.java @@ -0,0 +1,27 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils; + +import android.os.AsyncTask; + + +/** + */ +public class LambdaTask extends AsyncTask { + + TaskRunnable taskRunnable; + + + public LambdaTask(TaskRunnable taskRunnable) { + this.taskRunnable = taskRunnable; + } + + + @Override + protected Void doInBackground(Void... voids) { + taskRunnable.run(); + return null; + } + + public interface TaskRunnable { + void run(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/NetworkUtilsExtend.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/NetworkUtilsExtend.kt new file mode 100644 index 0000000000..1a8fe74cc1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/NetworkUtilsExtend.kt @@ -0,0 +1,127 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.mogo.eagle.core.utilcode.util.ThreadUtils +import com.mogo.eagle.core.utilcode.util.Utils + + +/** + * 监听网络状态 + */ +class NetworkUtilsExtend { + private val TAG = "NetworkUtilsExtend" + + interface NetworkCallbackListener { + fun onConnected(network: Network?) + fun onDisconnected() + fun onLinkChanged(network: Network?, linkProperties: LinkProperties?) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + class NetworkCallbackImpl : NetworkCallback() { + private val TAG = "NetworkCallbackImpl" + + object LazyHolder { + val INSTANCE = NetworkCallbackImpl() + } + + // 网络连接成功回调 + override fun onAvailable(network: Network) { + super.onAvailable(network) + Log.d(TAG, "网络连接成功回调 onAvailable: $network") + ThreadUtils.runOnUiThread { + for (networkCallbackListener in networkCallbackList) { + networkCallbackListener.onConnected(network) + } + } + } + + // 网络连接超时或网络不可达 + override fun onUnavailable() { + super.onUnavailable() + Log.e(TAG, "网络连接超时或网络不可达 onUnavailable") + } + + override fun onLost(network: Network) { + super.onLost(network) + Log.e(TAG, "网络已断开连接 onLost: $network") + ThreadUtils.runOnUiThread { + for (networkCallbackListener in networkCallbackList) { + networkCallbackListener.onDisconnected() + } + } + } + + // 网络正在丢失连接 + override fun onLosing(network: Network, maxMsToLive: Int) { + super.onLosing(network, maxMsToLive) + Log.d(TAG, "网络正在丢失连接 onLosing: $network") + } + + + // 网络状态变化 + override fun onCapabilitiesChanged(network: Network, cap: NetworkCapabilities) { + super.onCapabilitiesChanged(network, cap) + Log.d(TAG, "网络状态变化 onCapabilitiesChanged: $network, $cap") + if (cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + if (cap.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + Log.d(TAG, "网络状态变化 onCapabilitiesChanged: 网络类型为wifi") + } else if (cap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + Log.d(TAG, "网络状态变化 onCapabilitiesChanged: 蜂窝网络") + } else { + Log.d(TAG, "网络状态变化 onCapabilitiesChanged: 其他网络") + } + } + } + + // 网络连接属性变化 + override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) { + super.onLinkPropertiesChanged(network, lp) + Log.d(TAG, "网络连接属性变化 onLinkPropertiesChanged: $network, $lp") + ThreadUtils.runOnUiThread { + for (networkCallbackListener in networkCallbackList) { + networkCallbackListener.onLinkChanged(network, lp) + } + } + } + + // 访问的网络阻塞状态发生变化 + override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { + super.onBlockedStatusChanged(network, blocked) + Log.d(TAG, "访问的网络阻塞状态发生变化 onBlockedStatusChanged: $network, $blocked") + } + } + + companion object { + private val networkCallbackList = ArrayList() + fun startRegisterNetworkCallback() { + val networkRequest = NetworkRequest.Builder().build() + val connMgr = + Utils.getApp().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connMgr.registerNetworkCallback(networkRequest, NetworkCallbackImpl.LazyHolder.INSTANCE) + } + + fun stopRegisterNetworkCallback() { + val connMgr = + Utils.getApp().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connMgr.unregisterNetworkCallback(NetworkCallbackImpl.LazyHolder.INSTANCE) + } + + fun addNetworkCallback(callbackListener: NetworkCallbackListener) { + networkCallbackList.add(callbackListener) + } + + fun removeNetworkCallback(callbackListener: NetworkCallbackListener) { + networkCallbackList.remove(callbackListener) + } + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/PermissionUtil.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/PermissionUtil.java new file mode 100644 index 0000000000..d817522805 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/PermissionUtil.java @@ -0,0 +1,434 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils; + + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * author:chenjs + */ +public class PermissionUtil { + private static final String TAG = PermissionUtil.class.getSimpleName(); + private static final boolean LOG_FLAG = true;//日志标识 + + //日历 + private static final String[] Group_Calendar = { + Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR + }; + //照相机 + private static final String[] Group_Camera = { + Manifest.permission.CAMERA + }; + //通讯录 + private static final String[] Group_Contacts = { + Manifest.permission.WRITE_CONTACTS, Manifest.permission.GET_ACCOUNTS, + Manifest.permission.READ_CONTACTS + }; + //定位 + private static final String[] Group_Location = { + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION + }; + //麦克风 + private static final String[] Group_Microphone = { + Manifest.permission.RECORD_AUDIO + }; + //电话 + private static final String[] Group_Phone = { + Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE, + Manifest.permission.READ_CALL_LOG, Manifest.permission.WRITE_CALL_LOG, + Manifest.permission.ADD_VOICEMAIL, Manifest.permission.USE_SIP, + Manifest.permission.PROCESS_OUTGOING_CALLS + }; + //传感器 + private static final String[] Group_Sensors = { + Manifest.permission.BODY_SENSORS + }; + //短信 + private static final String[] Group_Sms = { + Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS, + Manifest.permission.RECEIVE_SMS, Manifest.permission.RECEIVE_MMS, + Manifest.permission.RECEIVE_WAP_PUSH + }; + //存储 + private static final String[] Group_Storage = { + Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + private static Map m_PermissionGroupList = null; + private static Map m_PermissionsMappingList = null; + + static { + initMap(); + } + + /** + * 通过权限组名来申请一组权限 + * + * @param context + * @param permissionGroupName + * @param requestCode + * @param listener + */ + public static void requestByGroupName(Activity context, String permissionGroupName, int requestCode, OnPermissionsListener listener) { + requestByGroupName(context, new String[]{permissionGroupName}, requestCode, listener); + } + + /** + * 通过权限组名来申请多组权限 + * + * @param context Activity上下文 + * @param pgNameArray 多个要申请的权限组名称 + * @param requestCode 请求码 + * @param listener 回调接口 + */ + public static void requestByGroupName(Activity context, String[] pgNameArray, int requestCode, OnPermissionsListener listener) { + showLog("requestByPermissionGroup"); + try { + //如果操作系统SDK级别在23之上(android6.0),就进行动态权限申请 + if (Build.VERSION.SDK_INT >= 23 && pgNameArray != null) { + String[] permissionsList = getAppPermissionsList(context);//应用权限列表 + ArrayList targetList = new ArrayList<>(); + if (permissionsList == null || permissionsList.length == 0) { + showLog("获得权限列表为空"); + return; + } + + for (String groupName : pgNameArray) { + ArrayList tmpPermissionList = isPermissionDeclared(permissionsList, groupName); + if (tmpPermissionList == null) {//未找到 + showLog("未找到[" + groupName + "]中的权限"); + continue; + } + + for (int i = 0; i < tmpPermissionList.size(); i++) { + //判断是否拥有权限 + int nRet = ContextCompat.checkSelfPermission(context, tmpPermissionList.get(i)); + if (nRet != PackageManager.PERMISSION_GRANTED) { + targetList.add(tmpPermissionList.get(i)); + } + } + } + + if (targetList.size() > 0) { + showLog("进行以下权限申请:" + targetList.toString()); + String[] sList = targetList.toArray(new String[0]); + ActivityCompat.requestPermissions(context, sList, requestCode); + } else { + showLog("全部权限都已授权"); + if (listener != null) { + listener.onPermissionsOwned(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 通过权限名来申请一组权限 + * + * @param context + * @param permission + * @param requestCode + * @param listener + */ + public static void requestByPermissionName(Activity context, String permission, int requestCode, OnPermissionsListener listener) { + requestByPermissionName(context, new String[]{permission}, requestCode, listener); + } + + /** + * 通过权限名来申请多组权限 + * + * @param context Activity上下文 + * @param permissionArray 多个要申请的权限名称 + * @param requestCode 请求码 + * @param listener 回调接口 + */ + public static void requestByPermissionName(Activity context, String[] permissionArray, int requestCode, OnPermissionsListener listener) { + showLog("requestPermissions"); + try { + //如果操作系统SDK级别在23之上(android6.0),就进行动态权限申请 + if (Build.VERSION.SDK_INT >= 23 && permissionArray != null) { + ArrayList targetList = new ArrayList<>(); + for (String strPermission : permissionArray) { + //判断是否拥有权限 + int nRet = ContextCompat.checkSelfPermission(context, strPermission); + if (nRet != PackageManager.PERMISSION_GRANTED) { + targetList.add(strPermission); + } + } + + if (targetList.size() > 0) { + showLog("进行以下权限申请:" + targetList.toString()); + String[] sList = targetList.toArray(new String[0]); + ActivityCompat.requestPermissions(context, sList, requestCode); + } else { + showLog("全部权限都已授权"); + if (listener != null) { + listener.onPermissionsOwned(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 针对申请权限时的用户操作进行处理 + * + * @param context + * @param permissions 申请的权限 + * @param grantResults 各权限的授权状态 + * @param listener 回调接口 + * @param controlFlag 控制标识,用于判断当响应禁止列表后,是否继续处理可再申请列表(避免出现同时处理禁止列表和可再申请列表,互相干扰,比如弹出两个提示框) + */ + public static void onRequestPermissionsResult(Activity context, String[] permissions, int[] grantResults, OnPermissionsListener listener, boolean controlFlag) { + try { + ArrayList requestList = new ArrayList<>();//可再申请列表 + ArrayList banList = new ArrayList<>();//禁止列表 + for (int i = 0; i < permissions.length; i++) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + showLog("[" + permissions[i] + "]权限授权成功"); + } else { + boolean nRet = ActivityCompat.shouldShowRequestPermissionRationale(context, permissions[i]); + //Log.i(TAG,"shouldShowRequestPermissionRationale nRet="+nRet); + if (nRet) {//允许重新申请 + requestList.add(permissions[i]); + } else {//禁止申请 + banList.add(permissions[i]); + } + } + } + + do { + //优先对禁止列表进行判断 + if (banList.size() > 0) { + if (listener != null) { + listener.onPermissionsForbidden(permissions, grantResults, banList); + } + if (!controlFlag) {//对禁止列表处理后,且控制标识为false,则跳过对可再申请列表的处理 + break; + } + } + if (requestList.size() > 0) { + if (listener != null) { + listener.onPermissionsDenied(permissions, grantResults, requestList); + } + } + if (banList.size() == 0 && requestList.size() == 0) { + showLog("权限授权成功"); + if (listener != null) { + listener.onPermissionsSucceed(); + } + } + } while (false); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 判断权限状态 + * + * @param context + * @param permission 权限名 + * @return + */ + public static boolean checkPermission(Context context, String permission) { + try { + //如果操作系统SDK级别在23之上(android6.0),就进行动态权限申请 + if (Build.VERSION.SDK_INT >= 23) { + int nRet = ContextCompat.checkSelfPermission(context, permission); + showLog("checkSelfPermission nRet=" + nRet); + return nRet == PackageManager.PERMISSION_GRANTED; + } + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 获得当前应用清单中的权限列表 + * + * @param context 应用上下文 + * @return + */ + public static String[] getAppPermissionsList(Context context) { + try { + PackageManager packageManager = context.getApplicationContext().getPackageManager(); + String packageName = context.getApplicationContext().getPackageName(); + String[] array = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS).requestedPermissions; + return array; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 判断权限列表中是否声明了指定权限组中的权限 + * + * @param permissionList 权限列表 + * @param permissionGroup 权限组名 + * @return 存在则返回找到的权限组权限,否则返回null + */ + public static ArrayList isPermissionDeclared(String[] permissionList, String permissionGroup) { + try { + if (permissionList != null && permissionGroup != null) { + String[] pmGroup = m_PermissionGroupList.get(permissionGroup); + if (pmGroup != null) { + ArrayList arrayList = new ArrayList<>(); + //遍历 + for (int i = 0; i < pmGroup.length; i++) { + String strPermission = pmGroup[i]; + for (int j = 0; j < permissionList.length; j++) { + if (strPermission.equals(permissionList[j])) {//找到指定权限组中的权限 + arrayList.add(strPermission); + break; + } + } + } + if (arrayList.size() == 0) { + return null; + } + return arrayList; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 获得传入的权限名列表对应的中文名称 + * + * @param permissionList 权限名列表 + * @return 集合 + */ + public static Set getPermissionsNameByChinese(String[] permissionList) { + try { + if (permissionList != null) { + HashSet nameSet = new HashSet<>();//确保集合元素不重复 + String tmpName; + for (String strPermission : permissionList) { + tmpName = m_PermissionsMappingList.get(strPermission); + if (tmpName != null) { + nameSet.add(tmpName); + } + } + return nameSet; + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static void initMap() { + if (m_PermissionGroupList == null) { + m_PermissionGroupList = new HashMap<>(); + m_PermissionGroupList.put(Manifest.permission_group.CALENDAR, Group_Calendar); + m_PermissionGroupList.put(Manifest.permission_group.CAMERA, Group_Camera); + m_PermissionGroupList.put(Manifest.permission_group.CONTACTS, Group_Contacts); + m_PermissionGroupList.put(Manifest.permission_group.LOCATION, Group_Location); + m_PermissionGroupList.put(Manifest.permission_group.MICROPHONE, Group_Microphone); + m_PermissionGroupList.put(Manifest.permission_group.PHONE, Group_Phone); + m_PermissionGroupList.put(Manifest.permission_group.SENSORS, Group_Sensors); + m_PermissionGroupList.put(Manifest.permission_group.SMS, Group_Sms); + m_PermissionGroupList.put(Manifest.permission_group.STORAGE, Group_Storage); + } + + if (m_PermissionsMappingList == null) { + m_PermissionsMappingList = new HashMap<>(); + //日历 + for (String strPermission : Group_Calendar) { + m_PermissionsMappingList.put(strPermission, "日历"); + } + //照相机 + for (String strPermission : Group_Camera) { + m_PermissionsMappingList.put(strPermission, "摄像头"); + } + //通讯录 + for (String strPermission : Group_Contacts) { + m_PermissionsMappingList.put(strPermission, "通讯录"); + } + //定位 + for (String strPermission : Group_Location) { + m_PermissionsMappingList.put(strPermission, "位置"); + } + //麦克风 + for (String strPermission : Group_Microphone) { + m_PermissionsMappingList.put(strPermission, "麦克风"); + } + //电话 + for (String strPermission : Group_Phone) { + m_PermissionsMappingList.put(strPermission, "电话"); + } + //传感器 + for (String strPermission : Group_Sensors) { + m_PermissionsMappingList.put(strPermission, "传感器"); + } + //短信 + for (String strPermission : Group_Sms) { + m_PermissionsMappingList.put(strPermission, "短信"); + } + //存储 + for (String strPermission : Group_Storage) { + m_PermissionsMappingList.put(strPermission, "存储"); + } + } + } + + private static void showLog(String str) { + if (LOG_FLAG) { + Log.i(TAG, str); + } + } + + public interface OnPermissionsListener { + /** + * 权限都已拥有时的处理 + */ + void onPermissionsOwned(); + + /** + * 权限被禁止时的处理 + * + * @param permissions 申请的全部权限 + * @param grantResults 各权限的授权状态 + * @param pmList 禁止申请的权限列表 + */ + void onPermissionsForbidden(String[] permissions, int[] grantResults, ArrayList pmList); + + /** + * 权限被拒绝时的处理 + * + * @param permissions + * @param grantResults + * @param pmList 可再申请的权限列表 + */ + void onPermissionsDenied(String[] permissions, int[] grantResults, ArrayList pmList); + + /** + * 权限申请成功时的处理 + */ + void onPermissionsSucceed(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/ToastUtil.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/ToastUtil.java new file mode 100644 index 0000000000..44229462d0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/ToastUtil.java @@ -0,0 +1,38 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils; + +import android.content.Context; +import android.view.Gravity; +import android.widget.Toast; + +import java.lang.ref.WeakReference; + +public class ToastUtil { + private static WeakReference toastRef; + + public static void showToastCenter(Context context, String msg) { + showToastCenter(context, msg, Toast.LENGTH_SHORT); + } + + public static void showToastCenter(Context context, String msg, int duration) { + Toast toast = (toastRef != null) ? toastRef.get() : null; + if (toast == null) { + toast = Toast.makeText(context.getApplicationContext(), "", duration); //如果有居中显示需求 + toast.setGravity(Gravity.CENTER, 0, 0); + toastRef = new WeakReference<>(toast); + } + toast.setText(msg); + toast.show(); + } + + + public static void destroyToast() { + if (toastRef != null) { + Toast toast = toastRef.get(); + if (toast != null) { + toast.cancel(); + } + toastRef.clear(); + toastRef = null; + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/Utils.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/Utils.java new file mode 100644 index 0000000000..ff2862f886 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/common/utils/Utils.java @@ -0,0 +1,57 @@ +package com.zhjt.mogo_core_function_devatools.rviz.common.utils; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.Log; + + +import com.mogo.eagle.core.utilcode.util.RegexUtils; + +import java.lang.reflect.Field; + +public class Utils { + private static int sbar = -1; + // 获取系统状态栏高度 + public static int getSysBarHeight(Context contex) { + if (sbar == -1) { + Class c; + Object obj; + Field field; + int x; + sbar = 0; + try { + c = Class.forName("com.android.internal.R$dimen"); + obj = c.newInstance(); + field = c.getField("status_bar_height"); + x = Integer.parseInt(field.get(obj).toString()); + sbar = contex.getResources().getDimensionPixelSize(x); + } catch (Exception e1) { + e1.printStackTrace(); + } + } + Log.i("dddd","dddd=sbar="+sbar); + return sbar; + } + + /** + * 判断当前设备是手机还是平板,代码来自 Google I/O App for Android + * + * @param context + * @return 平板返回 True,手机返回 False + */ + public static boolean isPad(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } + + public static String getIPLastSegment(String ipv4) { + if (RegexUtils.isIP(ipv4)) { + int index = ipv4.lastIndexOf("."); + if (index > -1) { + ipv4 = ipv4.substring(index + 1); + } + } + return ipv4; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/AppConfigInfo.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/AppConfigInfo.kt new file mode 100644 index 0000000000..9d6ae97ccf --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/AppConfigInfo.kt @@ -0,0 +1,23 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +object AppConfigInfo { + + // 工控相关信息 + //车牌号 + @Volatile + var plateNumber: String = "" + + //工控机MAC地址 + @Volatile + var iPCMacAddress: String = "" + + //工控机DockerVersion + @Volatile + var dockerVersion: String = "" + + @Volatile + var mapVersion: Int = 0//解析后的域控版本 例如3.6.0 结果为30600 + + @Volatile + var isSupportFM: Boolean = false//是否支持FM数据 +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseSource.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseSource.kt new file mode 100644 index 0000000000..1a64cf5ebc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseSource.kt @@ -0,0 +1,13 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +enum class DiagnoseSource( + val msg: String, + val desc: String +) { + NET("NET:", "设备网络"), + PAD("PAD:", "设备"), + ADAS("ADAS:", "ADAS"), + SSH("SSH:", "远程连接"), + MC("MC:", "调试工具"); + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseType.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseType.kt new file mode 100644 index 0000000000..8f21923fc0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/DiagnoseType.kt @@ -0,0 +1,5 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +enum class DiagnoseType { + NORMAL, SUCCEED, FAILED +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/EventKey.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/EventKey.kt new file mode 100644 index 0000000000..99e6eae060 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/EventKey.kt @@ -0,0 +1,23 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant; + +object EventKey { + const val QUERY_ROS_HOST_STATUS = "query_ros_host_status" //查询ros 主机状态 + const val REMOVE_ROS_HOST_ITEM = "remove_ros_host_item" //删除ros主机 + const val QUERY_DOCKER_PS = "query_docker_ps" + const val QUERY_DISK_STATUS = "query_disk_status"//查询磁盘信息 + const val DOCKER_STATUS = "docker_status"//Docker状态 + const val QUERY_STARTUP_CONFIG = "query_startup_config"//查询配置文件列表 + const val UPDATE_ADAS_CONNECT_STATE = "update_adas_connect_state" + const val UPDATE_CAR_CONFIG_STATE = "update_car_config_state" + const val QUERY_DOCKER_CONFIG_CONTENT = "query_docker_config_content"//查询配置文件内容 + const val SEND_CLOUD_MAP_VERSION = "send_cloud_map_version"//发送云端map版本 + const val SEND_ROS_MASTER_MAP_VERSION = "send_ros_master_map_version"//发送ROS Master MAP版本信息 + const val SEND_HD_MAP_VERSION = "send_hd_map_version"//发送HD地图版本信息 + const val INIT_SENSOR_CAMERA = "init_Sensor_Camera"//初始化相机个数,不同车型相机数量和位置不同 + const val UPDATE_FAULT_CODE_DATA = "update_fault_code_data"//更新故障码item + const val SEND_ROS_HOST_COUNT = "send_ros_host_count"//发送主机个数 + const val SEND_FM_INFO_TO_OVERVIEW_FRAGMENT = "send_fm_info_to_overview_fragment"//FM数据发送到预览界面 + const val SEND_IS_SUPPORT_FM = "send_is_support_fm"//FM数据是否支持 + const val UPDATE_SYSTEM_RESOURCE_RED_DOT = "update_system_resource_red_dot"//更新资源界面异常个数通知 + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultLevel.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultLevel.kt new file mode 100644 index 0000000000..e905fd5db5 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultLevel.kt @@ -0,0 +1,123 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +import android.text.TextUtils +import com.zhjt.mogo_core_function_devatools.rviz.R + +/** + * 故障等级 + */ +enum class FaultLevel( + val policyCode: String, + val faultLevel: String, + val msg: String, + val colorRes: Int, + val compareLevel: Int//排序等级 +) { + LEVEL_UNKNOWN("UNKNOWN", "-1", "未知", R.color.rviz_fmd_fm_fault_level_unknown, Int.MIN_VALUE),//仅统计(认为健康) + LEVEL0("FM_DP_NO_ACTION", "0", "仅统计(认为健康)", R.color.rviz_fmd_fm_fault_level0, 0),//仅统计(认为健康) + LEVEL1("FM_DP_ONLY_WARNING", "1", "警示", R.color.rviz_fmd_fm_fault_level1, 1),//警示(仅展示) + LEVEL2("FM_DP_SPEED_LIMIT1", "2", "一级降速行驶", R.color.rviz_fmd_fm_fault_level2, 2),//一级降速行驶 + LEVEL3("FM_DP_SPEED_LIMIT2", "2", "二级降速行驶", R.color.rviz_fmd_fm_fault_level2, 3),//二级降速行驶 + LEVEL4("FM_DP_SPEED_LIMIT3", "2", "三级降速行驶", R.color.rviz_fmd_fm_fault_level2, 4),//三级降速行驶 + LEVEL5("FM_DP_PNC_CHOOSE_STOP", "3", "择机靠边停车", R.color.rviz_fmd_fm_fault_level3, 5),//择机靠边停车 + LEVEL6("FM_DP_COMFORTABLE_STOP", "3", "立刻舒适停车", R.color.rviz_fmd_fm_fault_level3, 6),//立刻舒适停车 + LEVEL7("FM_DP_EMERGENCY_STOP", "4", "就地紧急停车", R.color.rviz_fmd_fm_fault_level4, 7),//就地紧急停车 + + + SSH_CONNECT_ERROR("SSH_CONNECT_ERROR", "100", "SSH连接异常", R.color.rviz_fmd_connect_status_disconnected, 100);//就地紧急停车 + + companion object { + @JvmStatic + val ALL = values().filter { it != SSH_CONNECT_ERROR }//所有等级 不包含自定义 + private val stopPolicyCodes = setOf( + LEVEL5.policyCode, + LEVEL6.policyCode, + LEVEL7.policyCode, + ) + private val stopFaultLevels = setOf( + LEVEL5.faultLevel, + LEVEL6.faultLevel, + LEVEL7.faultLevel, + ) + + @JvmStatic + fun getOrder(policyCode: String?): Int { + if (policyCode.isNullOrEmpty()) return LEVEL_UNKNOWN.compareLevel + val levelByPolicyCode = ALL.find { it.policyCode == policyCode } + if (levelByPolicyCode != null) { + return levelByPolicyCode.compareLevel + } + return LEVEL_UNKNOWN.compareLevel + } + + @JvmStatic + fun isStopFault(policyCode: String?, faultLevel: String?): Boolean? { + if (policyCode.isNullOrEmpty() && faultLevel.isNullOrEmpty()) return null//未知故障等级 + // 检查 policyCode 是否属于需要停车的情况 + if (policyCode in stopPolicyCodes) { + return true + } + // 检查 faultLevel 是否为 "3" 或 "4" + if (faultLevel in stopFaultLevels) { + return true + } + // 如果不符合任何条件,则返回 false + return false + } + + @JvmStatic + fun getColor(policyCode: String?, faultLevel: String?): Int { + if (policyCode.isNullOrEmpty() && faultLevel.isNullOrEmpty()) return LEVEL_UNKNOWN.colorRes + // 根据 policyCode 匹配对应的 FaultLevel + val levelByPolicyCode = ALL.find { it.policyCode == policyCode } + if (levelByPolicyCode != null) { + return levelByPolicyCode.colorRes + } + // 根据 faultLevel 匹配对应的 FaultLevel + val levelByFaultLevel = ALL.find { it.faultLevel == faultLevel } + if (levelByFaultLevel != null) { + return levelByFaultLevel.colorRes + } + + // 默认返回 LEVEL_UNKNOWN 的颜色 + return LEVEL_UNKNOWN.colorRes + + } + + @JvmStatic + fun getMessageAndColor(policyCode: String?, faultLevel: String?): Pair { + if (policyCode.isNullOrEmpty() && faultLevel.isNullOrEmpty()) return Pair( + LEVEL_UNKNOWN.msg, + LEVEL_UNKNOWN.colorRes + ) + // 根据 policyCode 匹配对应的 FaultLevel + val levelByPolicyCode = ALL.find { it.policyCode == policyCode } + if (levelByPolicyCode != null) { + return Pair( + levelByPolicyCode.msg, + levelByPolicyCode.colorRes + ) + } + // 根据 faultLevel 匹配对应的 FaultLevel + val levelByFaultLevel = ALL.find { it.faultLevel == faultLevel } + if (levelByFaultLevel != null) { + var msg = levelByFaultLevel.msg + if (TextUtils.equals(faultLevel, "2")) { + msg = "降速行驶" + } else if (TextUtils.equals(faultLevel, "3")) { + msg = "择机靠边/立刻舒适停车" + } + return Pair( + msg, + levelByFaultLevel.colorRes + ) + } + + // 默认返回 LEVEL_UNKNOWN 的颜色 + return Pair( + LEVEL_UNKNOWN.msg, + LEVEL_UNKNOWN.colorRes + ) + } + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultModuleId.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultModuleId.kt new file mode 100644 index 0000000000..2d99db82af --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultModuleId.kt @@ -0,0 +1,5 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +enum class FaultModuleId { + VehicleControl, HardwareDriver, Perception, Localization, Planning, Prediction, SSM, SM, FSM, OTH +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultSubModuleId.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultSubModuleId.kt new file mode 100644 index 0000000000..79f576ecf4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/FaultSubModuleId.kt @@ -0,0 +1,147 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +/** + * 子模块名 + */ +enum class FaultSubModuleId( + val moduleId: FaultModuleId, + val subModuleId: String, + val subModuleFuzzyId: String,//模糊匹配ID + val subName: String +) { + HardwareDriver_LidarDriver(FaultModuleId.HardwareDriver, "*LidarDriver", "lidar", "激光雷达驱动"), + HardwareDriver_CameraDriver(FaultModuleId.HardwareDriver, "*CameraDriver", "camera", "摄像头驱动"), + HardwareDriver_RadarDriver(FaultModuleId.HardwareDriver, "*RadarDriver", "rada", "毫米波雷达驱动"), + + Perception_Fusion(FaultModuleId.Perception, "Fusion", "fusion", "后融合"), + Perception_MidFusion(FaultModuleId.Perception, "MidFusion", "midfusion", "中融合"), + Perception_RadarFusion(FaultModuleId.Perception, "RadarFusion", "radarfusion", "毫米波融合"), + Perception_XiaobaFusion(FaultModuleId.Perception, "XiaobaFusion", "xiaobafusion", "激光雷达点云融合"), + Perception_LidarFusion(FaultModuleId.Perception, "LidarFusion", "lidarfusion", "激光雷达点云融合"), + Perception_PerceptionCamera(FaultModuleId.Perception, "PerceptionCamera*", "perceptioncamera", "车道线相机感知"), + Perception_Camera2DFront(FaultModuleId.Perception, "Camera2DFront", "camera2dfront", "红绿灯相机感知"), + Perception_Camera2DHd(FaultModuleId.Perception, "Camera2DHd", "camera2dhd", "2D障碍物感知"), + Perception_Camera3D(FaultModuleId.Perception, "Camera3D", "camera3d", "Vidar感知"), + Perception_PerceptionLidar(FaultModuleId.Perception, "PerceptionLidar", "perceptionlidar", "激光雷达感知"), + + Localization_MSFLOC(FaultModuleId.Localization, "MSFLOC", "msfloc", "融合定位"), + Localization_SLAM(FaultModuleId.Localization, "SLAM", "slam", "slam"), + Localization_VAL(FaultModuleId.Localization, "VAL", "val", "视觉辅助定位"), + Localization_VSLAM(FaultModuleId.Localization, "VSLAM", "vslam", "vslam"), + + Prediction_DataPreProcessing(FaultModuleId.Prediction, "DataPreProcessing", "datapreprocessing", "数据预处理"), + Prediction_DataPostProcessing(FaultModuleId.Prediction, "DataPostProcessing", "datapostprocessing", "数据后处理"), + + Planning_DataPostProcessing(FaultModuleId.Planning, "DataPostProcessing", "datapostprocessing", "数据预处理"), + + VehicleControl_ElectrcFunctnAccssrs(FaultModuleId.VehicleControl, "ElectrcFunctnAccssrs", "electrcfunctnaccssrs", "电子电气功能附件"), + VehicleControl_VhclMtnCtrl(FaultModuleId.VehicleControl, "VhclMtnCtrl", "vhclmtnctrl", "车辆运动控制"), + VehicleControl_ElectrcAccssrsCntrl(FaultModuleId.VehicleControl, "ElectrcAccssrsCntrl", "electrcaccssrscntrl", "电气附件控制"), + VehicleControl_Cmm(FaultModuleId.VehicleControl, "Cmm", "cmm", "通讯"), + VehicleControl_VCU(FaultModuleId.VehicleControl, "VCU", "vcu", "整车控制器"), + VehicleControl_EMS(FaultModuleId.VehicleControl, "EMS", "ems", "发动机系统"), + VehicleControl_DMCU(FaultModuleId.VehicleControl, "DMCU", "dmcu", "驱动电机系统"), + VehicleControl_BMS(FaultModuleId.VehicleControl, "BMS", "bms", "高压电池系统"), + VehicleControl_TCU(FaultModuleId.VehicleControl, "TCU", "tcu", "变速箱系统"), + VehicleControl_EPS(FaultModuleId.VehicleControl, "EPS", "eps", "转向系统"), + VehicleControl_EBS(FaultModuleId.VehicleControl, "EBS", "ebs", "行车制动系统"), + VehicleControl_EPB(FaultModuleId.VehicleControl, "EPB", "epb", "驻车系统"), + VehicleControl_BCM(FaultModuleId.VehicleControl, "BCM", "bcm", "车身附件系统"), + VehicleControl_MCU(FaultModuleId.VehicleControl, "MCU", "mcu", "域控微控制单元"), + + SSM_NodeCheck(FaultModuleId.SSM, "NodeCheck", "nodecheck", "节点故障检查"), + SSM_AgentCheck(FaultModuleId.SSM, "AgentCheck", "agentcheck", "域控故障检查"), + SSM_NetCheck(FaultModuleId.SSM, "NetCheck", "netcheck", "网络检查"), + SSM_SsmFlt(FaultModuleId.SSM, "SsmFlt", "ssmflt", "SSM自身异常"), + + SM_Topmonito(FaultModuleId.SM, "Topmonito*", "topmonito", "系统软件-各域控性能监控"), + SM_Iotop(FaultModuleId.SM, "Iotop*", "iotop", "系统软件-各域控IO监控"), + SM_TimeSyncConfig(FaultModuleId.SM, "TimeSyncConfig*", "timesyncconfig", "系统硬件-时间同步检测"), + SM_LidarTimeSync(FaultModuleId.SM, "*LidarTimeSync", "lidartimesync", "系统硬件-各雷达授时检测"), + SM_CameraDeviceLink(FaultModuleId.SM, "CameraDeviceLink", "cameradevicelink", "系统硬件-各相机接线检测"), + SM_CanState(FaultModuleId.SM, "CanState-*", "canstate", "系统硬件-Can状态"), + SM_FpgaVersion(FaultModuleId.SM, "FpgaVersion*", "fpgaversion", "系统硬件-Fpga版本检测"), + + FSM_DataPostProcessing(FaultModuleId.FSM, "DataPostProcessing", "cmm", "数据预处理"),//FSM发出的FM数据子模块目前全部为Cmm 并没有文档中写的DataPostProcessing + + OTH_TELEMATICS(FaultModuleId.OTH, "TELEMATICS", "telematics", "通信模块"), + OTH_LED(FaultModuleId.OTH, "LED", "led", "LED屏幕管理"), + OTH_CUSTOM(FaultModuleId.OTH, "CUSTOM", "custom", "数据定制模块"), + OTH_DEPLOY(FaultModuleId.OTH, "DEPLOY", "deploy", "部署镜像模块"), + ; + + + companion object { + private val CLASSIFY = classify() + + private fun classify(): MutableMap> { + val map = mutableMapOf>() + val modules = FaultModuleId.values() + val subModules = FaultSubModuleId.values() + for (module in modules) { + var set = map[module.name] + if (set == null) { + set = mutableSetOf() + map[module.name] = set + } + for (id in subModules) { + if (module == id.moduleId) { + set.add(id) + } + } + } + return map + } + + /** + * isShowSubModule 只有 isShowTag = false 时才会生效 + */ + @JvmStatic + fun getName(faultId: String?, isShowTag: Boolean = false, isShowSubModule: Boolean = false): String { + var module = "" + var subodule = "" + if (!faultId.isNullOrEmpty()) { + val parts = faultId.split("_") + if (parts.size >= 3) { + module = parts[0] + subodule = parts[1] + } + } + return getName(module, subodule, isShowTag, isShowSubModule) + } + + @JvmStatic + fun getName(moduleId: String?, subModuleId: String?, isShowTag: Boolean = false, isShowSubModule: Boolean = false): String { + if (moduleId.isNullOrEmpty() || subModuleId.isNullOrEmpty()) return "" + val subModule = CLASSIFY[moduleId] ?: return "" + //精准匹配 + for (sub in subModule) { + if (sub.subModuleId.startsWith("*")) { + if (subModuleId.endsWith(sub.subModuleId.substring(1, sub.subModuleId.length))) { + return if (isShowTag) "【${sub.subName}】" else if (isShowSubModule) "${sub.subName}($subModuleId)" else sub.subName + } + } else if (sub.subModuleId.endsWith("*")) { + if (subModuleId.startsWith(sub.subModuleId.substring(0, sub.subModuleId.length - 1))) { + return if (isShowTag) "【${sub.subName}】" else if (isShowSubModule) "${sub.subName}($subModuleId)" else sub.subName + } + } else { + if (sub.subModuleId == subModuleId) { + return if (isShowTag) "【${sub.subName}】" else if (isShowSubModule) "${sub.subName}($subModuleId)" else sub.subName + } + } + } + //模糊匹配 + for (sub in subModule) { + if (subModuleId.lowercase().contains(sub.subModuleFuzzyId)) { + return if (isShowTag) "【${sub.subName}】" else if (isShowSubModule) "${sub.subName}($subModuleId)" else sub.subName + } + } + //匹配不到任何内容直接返回 + return if (isShowTag) "【${subModuleId}】" else subModuleId + + } + + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/MsgFmData.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/MsgFmData.kt new file mode 100644 index 0000000000..2c9ed4a39d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/MsgFmData.kt @@ -0,0 +1,164 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +import com.zhjt.mogo.adas.data.bean.MogoReport + +/** + * FM信息对照表 + */ +class MsgFmData{ + /** + * 当出现多个建议操作时,按照整车下电重启、请求人工驾驶接管、请求平行驾驶接管、系统重启、联系硬件工程师、联系运维工程师、联系软件工程师优先级递减的顺序,只展示最高优先级的内容 + */ + enum class FaultAction( + val faultType: String,//故障处理类别 + val faultAction: String,//故障处理行为定义 + val faultActionCode: String,//故障处理行为标识 + val faultActionDesc: String,//故障处理行为描述 + val faultLevel: Int//故障处理级别 + ){ + //请求平行驾驶接管 + FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER("恢复策略","请求平行驾驶接管","FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER","如planing出站时,规划失败",5), + //请求人工驾驶接管 + FM_ACT_NEED_MANNUAL_DERVING("恢复策略","请求人工驾驶接管","FM_ACT_NEED_MANNUAL_DERVING","如planing规划失败,且存在弱网判断",6), + //系统重启 + FM_ACT_NEED_RESTART_SYSTEM("恢复策略","系统重启","FM_ACT_NEED_RESTART_SYSTEM","如检测到出现多个节点奔溃",4), + //整车下电重启 + FM_ACT_MUST_VEHICLE_POWER_RESET("恢复策略","整车下电重启","FM_ACT_MUST_VEHICLE_POWER_RESET","如底盘无数据,需要下电重启",7), + //请联系硬件工程师 + FM_ACT_CONTACT_HARDWARE_ENGINEER("人工处理","请联系硬件工程师","FM_ACT_CONTACT_HARDWARE_ENGINEER","硬件接线,域控启动等故障",3), + //请联系运维工程师 + FM_ACT_CONTACT_OPERATIONS_ENGINEER("人工处理","请联系运维工程师","FM_ACT_CONTACT_OPERATIONS_ENGINEER","系统配置不对,网络等故障",2), + //请联系软件工程师 + FM_ACT_CONTACT_SOFTWARE_ENGINEER("人工处理","请联系软件工程师","FM_ACT_CONTACT_SOFTWARE_ENGINEER","节点挂掉,无法启动等故障",1); + + companion object{ + + //获取故障建议操作级别 + fun getFaultLevel(faultActionCode: String): Int{ + return when(faultActionCode){ + //请求平行驾驶接管 + FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER.faultActionCode -> FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER.faultLevel + //请求人工驾驶接管 + FM_ACT_NEED_MANNUAL_DERVING.faultActionCode -> FM_ACT_NEED_MANNUAL_DERVING.faultLevel + //系统重启 + FM_ACT_NEED_RESTART_SYSTEM.faultActionCode -> FM_ACT_NEED_RESTART_SYSTEM.faultLevel + //整车下电重启 + FM_ACT_MUST_VEHICLE_POWER_RESET.faultActionCode -> FM_ACT_MUST_VEHICLE_POWER_RESET.faultLevel + //请联系硬件工程师 + FM_ACT_CONTACT_HARDWARE_ENGINEER.faultActionCode -> FM_ACT_CONTACT_HARDWARE_ENGINEER.faultLevel + //请联系运维工程师 + FM_ACT_CONTACT_OPERATIONS_ENGINEER.faultActionCode ->FM_ACT_CONTACT_OPERATIONS_ENGINEER.faultLevel + //请联系软件工程师 + FM_ACT_CONTACT_SOFTWARE_ENGINEER.faultActionCode -> FM_ACT_CONTACT_SOFTWARE_ENGINEER.faultLevel + else -> 0 + } + } + + //获取故障建议操作 + fun getFaultAction(faultActionLevel: Int): String{ + return when(faultActionLevel){ + //请求平行驾驶接管 + FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER.faultLevel -> FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER.faultAction + //请求人工驾驶接管 + FM_ACT_NEED_MANNUAL_DERVING.faultLevel -> FM_ACT_NEED_MANNUAL_DERVING.faultAction + //系统重启 + FM_ACT_NEED_RESTART_SYSTEM.faultLevel -> FM_ACT_NEED_RESTART_SYSTEM.faultAction + //整车下电重启 + FM_ACT_MUST_VEHICLE_POWER_RESET.faultLevel -> FM_ACT_MUST_VEHICLE_POWER_RESET.faultAction + //请联系硬件工程师 + FM_ACT_CONTACT_HARDWARE_ENGINEER.faultLevel -> FM_ACT_CONTACT_HARDWARE_ENGINEER.faultAction + //请联系运维工程师 + FM_ACT_CONTACT_OPERATIONS_ENGINEER.faultLevel ->FM_ACT_CONTACT_OPERATIONS_ENGINEER.faultAction + //请联系软件工程师 + FM_ACT_CONTACT_SOFTWARE_ENGINEER.faultLevel -> FM_ACT_CONTACT_SOFTWARE_ENGINEER.faultAction + else -> "" + } + } + + //获取故障建议操作Code值 + fun getFaultActionCode(faultActionLevel: Int): String{ + return when(faultActionLevel){ + //请求平行驾驶接管 + FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER.faultLevel -> FM_ACT_NEED_PARALLEL_DERVING_TAKEOVER.faultActionCode + //请求人工驾驶接管 + FM_ACT_NEED_MANNUAL_DERVING.faultLevel -> FM_ACT_NEED_MANNUAL_DERVING.faultActionCode + //系统重启 + FM_ACT_NEED_RESTART_SYSTEM.faultLevel -> FM_ACT_NEED_RESTART_SYSTEM.faultActionCode + //整车下电重启 + FM_ACT_MUST_VEHICLE_POWER_RESET.faultLevel -> FM_ACT_MUST_VEHICLE_POWER_RESET.faultActionCode + //请联系硬件工程师 + FM_ACT_CONTACT_HARDWARE_ENGINEER.faultLevel -> FM_ACT_CONTACT_HARDWARE_ENGINEER.faultActionCode + //请联系运维工程师 + FM_ACT_CONTACT_OPERATIONS_ENGINEER.faultLevel ->FM_ACT_CONTACT_OPERATIONS_ENGINEER.faultActionCode + //请联系软件工程师 + FM_ACT_CONTACT_SOFTWARE_ENGINEER.faultLevel -> FM_ACT_CONTACT_SOFTWARE_ENGINEER.faultActionCode + else -> "" + } + } + + + } + + } + + enum class FaultResult( + val resultType: String,//影响类别 + val resultDefine: String,//故障影响定义 + val resultCode: String,//故障影响的标识 + val resultDesc: String//后果对应的处理描述 + ){ + //无法作业 + FM_RST_FUNCTION_LOST("功能影响","无法作业","FM_RST_FUNCTION_LOST","需要禁止作业,如扫盘故障,清扫车无法清扫作业"), + //无法开放运营 + FM_RST_FORBID_OPEN_WORK("功能影响","无法开放运营","FM_RST_FORBID_OPEN_WORK","需要禁止运营,如安全带故障,可以自驾,不能载人"), + //无法平行驾驶 + FM_RST_FORBID_PARALLEL_DERVING("驾驶影响","无法平行驾驶","FM_RST_FORBID_PARALLEL_DERVING","需要禁止平行驾驶"), + //无法自动驾驶 + FM_RST_FORBID_AUTOPILOT_DERVING("驾驶影响","无法自动驾驶","FM_RST_FORBID_AUTOPILOT_DERVING","需要禁止自驾"), + //无法手动驾驶 + FM_RST_FORBID_MANNUAL_DERVING("驾驶影响","无法手动驾驶","FM_RST_FORBID_MANNUAL_DERVING","需要禁止行车,如底盘存在故障,需要通知出来"), + //失控,无法策略停车 + FM_RST_OUT_OF_CONTROL("安全影响","失控,无法策略停车","FM_RST_OUT_OF_CONTROL","需要立即紧急通知到人,车辆失控,如驾驶中controller挂掉,发送102重启"); + + companion object{ + //获取结果原因描述 + fun getResultDefine(resultCode: String): String{ + return when(resultCode){ + //无法作业 + FM_RST_FUNCTION_LOST.resultCode -> FM_RST_FUNCTION_LOST.resultDefine + //无法开放运营 + FM_RST_FORBID_OPEN_WORK.resultCode -> FM_RST_FORBID_OPEN_WORK.resultDefine + //无法平行驾驶 + FM_RST_FORBID_PARALLEL_DERVING.resultCode -> FM_RST_FORBID_PARALLEL_DERVING.resultDefine + //无法自动驾驶 + FM_RST_FORBID_AUTOPILOT_DERVING.resultCode -> FM_RST_FORBID_AUTOPILOT_DERVING.resultDefine + //无法手动驾驶 + FM_RST_FORBID_MANNUAL_DERVING.resultCode -> FM_RST_FORBID_MANNUAL_DERVING.resultDefine + //失控,无法策略停车 + FM_RST_OUT_OF_CONTROL.resultCode -> FM_RST_OUT_OF_CONTROL.resultDefine + else -> "" + } + } + } + + } + + companion object{ + + @JvmStatic + fun getFmPolicyName(policyCode: String?): String{ + return when(policyCode){ + "FM_DP_NO_ACTION" -> "报告" + "FM_DP_ONLY_WARNING" -> "警示" + "FM_DP_SPEED_LIMIT1" -> "一级降速" + "FM_DP_SPEED_LIMIT2" -> "二级降速" + "FM_DP_SPEED_LIMIT3" -> "三级降速" + "FM_DP_PNC_CHOOSE_STOP" -> "择机靠边停车" + "FM_DP_COMFORTABLE_STOP" -> "立刻舒适停车" + "FM_DP_EMERGENCY_STOP" -> "就地紧急停车" + else -> "暂无" + } + } + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/SensorCamera.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/SensorCamera.kt new file mode 100644 index 0000000000..e25426023b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/constant/SensorCamera.kt @@ -0,0 +1,14 @@ +package com.zhjt.mogo_core_function_devatools.rviz.constant + +enum class SensorCamera(val key: String, val fileName: String, val title: String) { + DriversCameraSensing30("DriversCameraSensing30", "sensing30.launch", "前左30"), + DriversCameraSensing60("DriversCameraSensing60", "sensing60.launch", "前中60"), + DriversCameraSensing120("DriversCameraSensing120", "sensing120.launch", "前右120"), + DriversCameraSensing120Left("DriversCameraSensing120Left", "sensing120_left.launch", "左120"), + DriversCameraSensing120Back("DriversCameraSensing120Back", "sensing120_back.launch", "后120"), + DriversCameraSensing120Right( + "DriversCameraSensing120Right", + "sensing120_right.launch", + "右120" + ), +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ChangeDefaultConfigDialog.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ChangeDefaultConfigDialog.kt new file mode 100644 index 0000000000..5d3dff7d1b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ChangeDefaultConfigDialog.kt @@ -0,0 +1,111 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog + + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.text.TextUtils +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.EditText +import android.widget.TextView.OnEditorActionListener +import com.mogo.eagle.core.utilcode.util.RegexUtils +import com.mogo.eagle.core.utilcode.util.ToastUtils +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.config.SSHAccountConfig + +/** + * 修改默认配置弹窗 + */ +class ChangeDefaultConfigDialog( + context: Context +) : Dialog(context) { + private lateinit var inputIp: EditText + private lateinit var inputSshUsername: EditText + private lateinit var inputSshUserpwd: EditText + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.rviz_common_dialog_default_config) + window?.setBackgroundDrawable(null) + window?.setLayout( + 700, + WindowManager.LayoutParams.WRAP_CONTENT + ) +// window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + inputIp = findViewById(R.id.input_ip) + inputSshUsername = findViewById(R.id.input_ssh_username) + inputSshUserpwd = findViewById(R.id.input_ssh_userpwd) + inputIp.setOnEditorActionListener(onEditorActionListener) + inputSshUsername.setOnEditorActionListener(onEditorActionListener) + inputSshUserpwd.setOnEditorActionListener(onEditorActionListener) + inputIp.setText(SSHAccountConfig.getRosMasterIp()) + inputSshUsername.setText(SSHAccountConfig.getUserName()) + inputSshUserpwd.setText(SSHAccountConfig.getPassWord()) + + setOnDismissListener { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + this.window?.currentFocus?.windowToken, + 0 + ) + } + val btnCancel: Button = findViewById(R.id.btnCancel) + val btnConfirm: Button = findViewById(R.id.btnConfirm) + btnCancel.setOnClickListener { + dismiss() + } + btnConfirm.setOnClickListener { + onConfirm() + } + } + + private fun onConfirm() { + val enableIp = inputIp.text + if (TextUtils.isEmpty(enableIp)) { + SSHAccountConfig.removeRosMasterIp() + } else { + if (RegexUtils.isIP(enableIp)) { + val ip = enableIp.toString().trim() + SSHAccountConfig.setRosMasterIp(ip) + } else { + ToastUtils.showShort("请输入正确的IP") + return + } + } + val enableUserName = inputSshUsername.text + if (TextUtils.isEmpty(enableUserName)) { + SSHAccountConfig.removeUserName() + } else { + val userName = enableUserName.toString().trim() + SSHAccountConfig.setUserName(userName) + } + val enableUserPwd = inputSshUserpwd.text + if (TextUtils.isEmpty(enableUserPwd)) { + SSHAccountConfig.removePassWord() + } else { + val userPwd = enableUserPwd.toString().trim() + SSHAccountConfig.setPassWord(userPwd) + } + dismiss() + } + + private val onEditorActionListener = + OnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_NEXT) { + // 获取下一个焦点的资源 ID + val nextFocusId = v.nextFocusForwardId + val editText = findViewById(nextFocusId) +// editText.setSelection(editText.text.length) + editText.selectAll(); + } else if (actionId == EditorInfo.IME_ACTION_GO) { + onConfirm() + return@OnEditorActionListener true + } + false + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/CommonLoadingDialog.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/CommonLoadingDialog.kt new file mode 100644 index 0000000000..f9a22437de --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/CommonLoadingDialog.kt @@ -0,0 +1,66 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog + + +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.fragment.app.DialogFragment +import com.zhjt.mogo_core_function_devatools.rviz.R + +/** + * 加载Loading对话框 + */ +class CommonLoadingDialog : + DialogFragment { + + private val TAG: String = "LoadingDialog" + private val msg: String? + private var textColor = android.R.color.white + private lateinit var rootView: View + private lateinit var tvTitle: TextView + + constructor() : super() { + this.msg = null + } + + constructor(msg: String?) : super() { + this.msg = msg + } + + constructor(@ColorRes textColor: Int, msg: String?) : super() { + this.textColor = textColor + this.msg = msg + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + rootView = LayoutInflater.from(context) + .inflate(R.layout.rviz_fmd_dialog_common_loading, container, false) + tvTitle = rootView.findViewById(R.id.tvTitle) + return rootView + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (!TextUtils.isEmpty(msg)) { + tvTitle.text = msg + } + tvTitle.setTextColor(resources.getColor(textColor, null)) + } + + public fun setMsg(msg: String?) { + if (!TextUtils.isEmpty(msg)) { + tvTitle.text = msg + } + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/DockersDialog.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/DockersDialog.java new file mode 100644 index 0000000000..edac4d089f --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/DockersDialog.java @@ -0,0 +1,91 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.mogo.eagle.core.utilcode.util.SizeUtils; +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseDialog; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.Utils; +import com.zhjt.mogo_core_function_devatools.rviz.dialog.adapter.DockerListAdapter; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerBean; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerInfo; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; +import com.zhjt.mogo_core_function_devatools.rviz.widgets.MyLinearLayoutManager; + +import java.util.List; + +/** + * Docker列表展示 + */ +public class DockersDialog extends BaseDialog { + private final List list; + private final SSHHostBean host; + private RecyclerView dockerListView; + private TextView titleView; + private Button btnCancel; + + public DockersDialog(Context context, DockerBean dockerBean) { + super(context); + this.list = dockerBean.dockers; + this.host = dockerBean.host; + } + + @Override + protected void onViewCreateBefore() { + super.onViewCreateBefore(); + getWindow().setBackgroundDrawableResource(R.drawable.rviz_common_bg_dialog); + } + + @Override + protected View getContentViewResource() { + return getLayoutInflater().inflate(R.layout.rviz_fmd_dialog_dockers, null, false); + } + + @Override + protected void onViewInitBefore() { + super.onViewInitBefore(); + WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); + layoutParams.width = SizeUtils.dp2px(getContext().getResources().getDimension(R.dimen.dp_600)); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + getWindow().setAttributes(layoutParams); + } + + + @Override + protected void onViewInit() { + super.onViewInit(); + dockerListView = findViewById(R.id.docker_list_view); + titleView = findViewById(R.id.title_view); + btnCancel = findViewById(R.id.btn_cancel); + MyLinearLayoutManager linearLayoutManager = new MyLinearLayoutManager(getContext()); + linearLayoutManager.setOrientation(GridLayoutManager.VERTICAL); + dockerListView.setLayoutManager(linearLayoutManager); + DockerListAdapter adapter = new DockerListAdapter(); + adapter.setData(list); + dockerListView.setAdapter(adapter); + String ip = host.getHostname(); + titleView.setText(Utils.getIPLastSegment(ip) + "--Docker信息"); + } + + + @Override + protected void setOnListener() { + super.setOnListener(); + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FMDataShowDialog.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FMDataShowDialog.kt new file mode 100644 index 0000000000..f063edd6fe --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FMDataShowDialog.kt @@ -0,0 +1,76 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog + +import android.content.Context +import android.view.View +import android.view.View.OnClickListener +import android.widget.Button +import android.widget.TextView +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseDialog + + +class FMDataShowDialog(context: Context, private val code: String?, private val data: String?) : + BaseDialog(context), OnClickListener { + private lateinit var titleView: TextView + private lateinit var dataView: TextView + private lateinit var btnCancel: Button + + init { + + } + + override fun getContentViewResource(): View { + return layoutInflater.inflate(R.layout.rviz_fmd_dialog_fm_data_show, null, false) + } + + override fun onViewInitBefore() { + super.onViewInitBefore() + window?.setBackgroundDrawableResource(android.R.color.transparent) +// window?.setLayout( +// WindowManager.LayoutParams.WRAP_CONTENT, +// WindowManager.LayoutParams.WRAP_CONTENT +// ) + } + + + override fun onViewInit() { + super.onViewInit() + titleView = findViewById(R.id.title_view) + dataView = findViewById(R.id.data) + btnCancel = findViewById(R.id.btn_cancel) + + } + + + override fun onViewCreated() { + super.onViewCreated() + if (!code.isNullOrEmpty()) { + titleView.text = "“${code}”原始数据" + titleView.setSelected(true); + } + val msg = if (!data.isNullOrEmpty()) { + data + } else { + "域控发送数据异常,无法查看" + } + dataView.text = msg + } + + + override fun onDismiss() { + super.onDismiss() + } + + override fun setOnListener() { + super.setOnListener() + btnCancel.setOnClickListener(this) + } + + override fun onClick(v: View) { + val id = v.id + if (id == R.id.btn_cancel) { + dismiss() + + } + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FaultCodeDetailsDialog.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FaultCodeDetailsDialog.kt new file mode 100644 index 0000000000..cdee0d99cf --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/FaultCodeDetailsDialog.kt @@ -0,0 +1,357 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog + +import android.content.Context +import android.view.View +import android.view.View.OnClickListener +import android.widget.AdapterView +import android.widget.Button +import android.widget.ExpandableListView +import android.widget.ProgressBar +import android.widget.TextView +import com.google.protobuf.TextFormat +import com.mogo.eagle.core.utilcode.util.SizeUtils +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseDialog +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultSubModuleId +import com.zhjt.mogo_core_function_devatools.rviz.constant.MsgFmData +import com.zhjt.mogo_core_function_devatools.rviz.dialog.adapter.FaultCodeDetailsAdapter +import com.zhjt.mogo_core_function_devatools.rviz.model.db.FmCodeRepository +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmCodeEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmEntity +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +/** + * FM详细信息展示弹窗 + + */ +class FaultCodeDetailsDialog(context: Context?, private val fmEntity: FmEntity) : + BaseDialog(context), OnClickListener { + lateinit var adapter: FaultCodeDetailsAdapter + private var isExpand = false + private val expandMap = mutableMapOf() + private lateinit var listView: ExpandableListView + private lateinit var tvModuleTitle: TextView + private lateinit var tvModuleState: TextView + private lateinit var tvMsg: TextView + private lateinit var btnExpand: Button + private lateinit var btnCancel: Button + private lateinit var layoutHint: View + private lateinit var loadingView: ProgressBar + + init { + + } + + override fun getContentViewResource(): View { + return layoutInflater.inflate(R.layout.rviz_fmd_dialog_fault_code_details, null, false) + } + + + override fun onViewInitBefore() { + super.onViewInitBefore() + val metrics2 = context.resources.displayMetrics + val widthPixels = metrics2.widthPixels + val heightPixels = metrics2.heightPixels + window?.setLayout( +// SizeUtils.dp2px(1280F), +// SizeUtils.dp2px(740F) + widthPixels, + heightPixels - SizeUtils.dp2px(60F) + ) + } + + + override fun onViewInit() { + super.onViewInit() + listView = findViewById(R.id.list_view) + tvModuleTitle = findViewById(R.id.tvModuleTitle) + tvModuleState = findViewById(R.id.tvModuleState) + btnExpand = findViewById(R.id.btn_expand) + layoutHint = findViewById(R.id.layout_hint) + loadingView = findViewById(R.id.loading_view) + tvMsg = findViewById(R.id.tv_msg) + btnCancel = findViewById(R.id.btn_cancel) + + + adapter = FaultCodeDetailsAdapter(context) + listView.setAdapter(adapter) + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onViewCreated() { + super.onViewCreated() + tvModuleTitle.text = fmEntity.title + var msg = String.format("严重故障:%s个 ", fmEntity.stopFaultNum) + String.format( + "其他故障:%s个", + fmEntity.otherFaultNum + ) + if (fmEntity.unknownFaultNum != 0) { + msg += String.format(" 未知故障:%s个", fmEntity.unknownFaultNum) + } + tvModuleState.text = msg + btnExpand.setOnClickListener { + isExpand = !isExpand + btnExpand.text = if (isExpand) "折叠" else "展开" + for (index in 0 until adapter.groupCount) { + if (isExpand) { + listView.expandGroup(index) + } else { + listView.collapseGroup(index) + } + } + } + listView.setOnGroupExpandListener { groupPosition -> + // 在这里添加你想要执行的操作,当某个 Group 被展开时触发 + expandMap[groupPosition] = true + val allTrue = expandMap.all { it.value } + if (allTrue) { + isExpand = true + btnExpand.text = "折叠" + } + } + listView.setOnGroupCollapseListener { groupPosition -> + // 在这里添加你想要执行的操作,当某个 Group 被展开时触发 + expandMap[groupPosition] = false + val allFalse = expandMap.all { !it.value } + if (allFalse) { + isExpand = false + btnExpand.text = "展开" + } + } + listView.onItemLongClickListener = + AdapterView.OnItemLongClickListener { parent, view, position, id -> // 获取组或子项的信息 + val packedPosition: Long = listView.getExpandableListPosition(position) + val itemType = ExpandableListView.getPackedPositionType(packedPosition) + + if (itemType == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + // 这是子项长按事件 + val groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition) + val childPosition = ExpandableListView.getPackedPositionChild(packedPosition) + val entity = adapter.getChild(groupPosition, childPosition) + FMDataShowDialog(context, entity.faultCode, entity.originalData).show() + return@OnItemLongClickListener true + } else if (itemType == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { + // 这是组项长按事件 + val groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition) + return@OnItemLongClickListener true + } + false + }; + layoutHint.visibility = View.VISIBLE + val data = fmEntity.data + if (data.isEmpty()) { + loadingView.visibility = View.GONE + tvMsg.text = "数据异常" + } else { + GlobalScope.launch(Dispatchers.IO) { + val list = ArrayList>>() + val iterator = data.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + var fmCodeEntity: FmCodeEntity? = null + if (!item.faultId.isNullOrEmpty()) { + fmCodeEntity = FmCodeRepository.getFmCodeInfo(item.faultId) + } + if (fmCodeEntity == null) { + fmCodeEntity = FmCodeEntity() + fmCodeEntity.moduleId = "" + fmCodeEntity.subModuleId = "" + fmCodeEntity.faultId = "" + fmCodeEntity.faultCode = item.faultId + if (!item.faultId.isNullOrEmpty()) { + val parts = item.faultId.split("_") + if (parts.size >= 3) { + fmCodeEntity.moduleId = parts[0] + fmCodeEntity.subModuleId = parts[1] + fmCodeEntity.faultId = parts[2] + } + } + } + + if (!item.faultName.isNullOrEmpty()) { + fmCodeEntity.faultName = item.faultName + } + + if (!item.faultResultList.isNullOrEmpty()) { + val tem = getSystemImpact(item.faultResultList) + if (tem.isNotEmpty()) { + fmCodeEntity.systemImpact = tem + } + } + if (!item.faultActionList.isNullOrEmpty()) { + val tem = getTroubleshootingSuggestions(item.faultActionList) + if (tem.isNotEmpty()) { + fmCodeEntity.troubleshootingSuggestions = tem + } + } + if (!fmCodeEntity.faultReason.isNullOrEmpty() && fmCodeEntity.faultReason.contains( + "\n" + ) + ) { + fmCodeEntity.faultReason = fmCodeEntity.faultReason.replace("\n", " ") + } + if (!fmCodeEntity.systemImpact.isNullOrEmpty() && fmCodeEntity.systemImpact.contains( + "\n" + ) + ) { + fmCodeEntity.systemImpact = fmCodeEntity.systemImpact.replace("\n", " ") + } + if (!fmCodeEntity.troubleshootingSuggestions.isNullOrEmpty() && fmCodeEntity.troubleshootingSuggestions.contains( + "\n" + ) + ) { + fmCodeEntity.troubleshootingSuggestions = + fmCodeEntity.troubleshootingSuggestions.replace("\n", " ") + } + fmCodeEntity.faultTime = item.faultTime + fmCodeEntity.systemDegradationStrategy = item.policyCode + fmCodeEntity.faultLevel = item.faultLevel + fmCodeEntity.originalData = + TextFormat.printer().escapingNonAscii(false).printToString(item) + fmCodeEntity.subModuleName = FaultSubModuleId.getName( + fmCodeEntity.moduleId, + fmCodeEntity.subModuleId, + false, + true + ) + addList(list, fmCodeEntity) + } + val (unclassifiedPairs, otherPairs) = list.partition { it.first == "异常数据" } + if (unclassifiedPairs.isNotEmpty()) { + list.clear() + list.addAll(otherPairs) + list.addAll(unclassifiedPairs) + } + for ((index, item) in list.withIndex()) { + expandMap[index] = false + } + withContext(Dispatchers.Main) { + layoutHint.visibility = View.GONE + adapter.setData(list) + if (adapter.groupCount == 1) { + listView.expandGroup(0) + } + } + } + } + } + + + private fun addList( + list: ArrayList>>, + fmCodeEntity: FmCodeEntity + ) { + if (fmCodeEntity.subModuleId.isEmpty()) { + addDetails(list, "异常数据", fmCodeEntity) + } else { + addDetails(list, fmCodeEntity.subModuleId, fmCodeEntity) + } + } + + private fun addDetails( + list: ArrayList>>, + key: String, + fmCodeEntity: FmCodeEntity + ) { + val existingPair = list.find { it.first == key } + if (existingPair != null) { + existingPair.second.add(fmCodeEntity) + } else { + val newList = ArrayList() + newList.add(fmCodeEntity) + val newPair = Pair(key, newList) + list.add(newPair) + // 执行其他操作 + } + } + + private fun getTroubleshootingSuggestions(faultActionList: List): String { + return if (faultActionList.isEmpty()) { + "" + } else { + val receiveFaultLevel = ArrayList() + faultActionList.forEach { action -> + //如果不包含此故障Level,则进行添加 + if (!receiveFaultLevel.contains(MsgFmData.FaultAction.getFaultLevel(action)) && MsgFmData.FaultAction.getFaultLevel( + action + ) != 0 + ) { + receiveFaultLevel.add(MsgFmData.FaultAction.getFaultLevel(action)) + } + } + //对faultLevel集合进行排序,按照顺序输出建议操作 + if (receiveFaultLevel.size > 0) { + val faultActionStr: StringBuilder = StringBuilder() + receiveFaultLevel.sort() + receiveFaultLevel.reverse() + receiveFaultLevel.forEach { level -> + if (MsgFmData.FaultAction.getFaultAction(level).isNotBlank()) { + faultActionStr.append(MsgFmData.FaultAction.getFaultAction(level)) + } + if (MsgFmData.FaultAction.getFaultActionCode(level).isNotBlank()) { + faultActionStr.append("(") + faultActionStr.append(MsgFmData.FaultAction.getFaultActionCode(level)) + faultActionStr.append(")") + } + if (MsgFmData.FaultAction.getFaultAction(level) + .isNotBlank() || MsgFmData.FaultAction.getFaultActionCode(level) + .isNotBlank() + ) { + faultActionStr.append(" ") + } + } + faultActionStr.toString() + } else { + "暂无" + } + } + } + + private fun getSystemImpact(faultResultList: List): String { + return if (faultResultList.isEmpty()) { + "" + } else { + val fmFaultResult = StringBuilder() + faultResultList.forEach { result -> + if (MsgFmData.FaultResult.getResultDefine(result).isNotBlank()) { + fmFaultResult.append(MsgFmData.FaultResult.getResultDefine(result)) + } + if (result.isNotBlank()) { + fmFaultResult.append("(") + fmFaultResult.append(result) + fmFaultResult.append(")") + } + if (MsgFmData.FaultResult.getResultDefine(result) + .isNotBlank() || result.isNotBlank() + ) { + fmFaultResult.append(" ") + } + } + fmFaultResult.toString() + } + } + + + override fun onDismiss() { + super.onDismiss() + } + + override fun setOnListener() { + super.setOnListener() + btnCancel.setOnClickListener(this) + } + + override fun onClick(v: View) { + val id = v.id + if (id == R.id.btn_cancel) { + dismiss() + } + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/InputUserPwdDialog.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/InputUserPwdDialog.kt new file mode 100644 index 0000000000..b24e66d71e --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/InputUserPwdDialog.kt @@ -0,0 +1,101 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.text.TextUtils +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener +import com.mogo.eagle.core.utilcode.util.ToastUtils +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean + +/** + * 密码输入弹窗 + */ +class InputUserPwdDialog( + context: Context, + private val host: SSHHostBean, + private val mOnClickListener: OnClickListener +) : Dialog(context) { + + + private val TAG: String = "DisconnectDialog" + private lateinit var inputRosUserName: EditText + private lateinit var inputRosPassword: EditText + + public interface OnClickListener { + fun onConfirm() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val rootView = layoutInflater.inflate(R.layout.rviz_fmd_dialog_fmd_user_pwd, null, false) + val titleView: TextView = findViewById(R.id.title) + val inputRosUserName: EditText = findViewById(R.id.input_ros_user_name) + val inputRosPassword: EditText = findViewById(R.id.input_ros_password) + val btnCancel: Button = findViewById(R.id.btnCancel) + val btnConfirm: Button = findViewById(R.id.btnConfirm) + setContentView(rootView) + window?.setBackgroundDrawable(null) + titleView.text = host.hostname + inputRosUserName.setOnEditorActionListener(onEditorActionListener) + inputRosPassword.setOnEditorActionListener(onEditorActionListener) + if (!TextUtils.isEmpty(host.username)) { + inputRosUserName.setText(host.username) + } + if (!TextUtils.isEmpty(host.userPwd)) { + inputRosPassword.setText(host.userPwd) + } + setOnDismissListener { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + this.window?.currentFocus?.windowToken, + 0 + ) + } + btnCancel.setOnClickListener { + dismiss() + } + btnConfirm.setOnClickListener { + onConfirm() + } + } + + private fun onConfirm() { + val enableUser = inputRosUserName.text + if (TextUtils.isEmpty(enableUser)) { + ToastUtils.showShort("请输入用户名") + return@onConfirm + } + val enablePwd = inputRosPassword.text + if (TextUtils.isEmpty(enablePwd)) { + ToastUtils.showShort("请输入密码") + return@onConfirm + } + host.username = enableUser.toString().trim() + host.userPwd = enablePwd.toString().trim() + mOnClickListener.onConfirm() + dismiss() + } + + private val onEditorActionListener = + OnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_NEXT) { + // 获取下一个焦点的资源 ID + val nextFocusId = v.nextFocusForwardId + val editText = findViewById(nextFocusId) + editText.setSelection(editText.text.length) + // editText.selectAll(); + } else if (actionId == EditorInfo.IME_ACTION_GO) { + onConfirm() + return@OnEditorActionListener true + } + false + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ShowConfigDialog.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ShowConfigDialog.java new file mode 100644 index 0000000000..773ffa39c0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/ShowConfigDialog.java @@ -0,0 +1,84 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog; + +import android.content.Context; +import android.graphics.Typeface; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import com.mogo.eagle.core.utilcode.util.SizeUtils; +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseDialog; + + +/** + * 配置展示 + */ +public class ShowConfigDialog extends BaseDialog { + private String data; + private final String title; + private TextView dataView; + private TextView titleView; + private Button btnCancel; + + public ShowConfigDialog(Context context, String title, String data) { + super(context); + this.title = title; + this.data = data; + } + + @Override + protected void onViewCreateBefore() { + super.onViewCreateBefore(); + getWindow().setBackgroundDrawableResource(R.drawable.rviz_common_bg_dialog); + } + + @Override + protected View getContentViewResource() { + return getLayoutInflater().inflate(R.layout.rviz_fmd_dialog_show_config, null, false); + } + + @Override + protected void onViewInitBefore() { + super.onViewInitBefore(); + Window window = getWindow(); + if (window != null) { + window.setLayout(SizeUtils.dp2px(getContext().getResources().getDimension(R.dimen.dp_600)), WindowManager.LayoutParams.WRAP_CONTENT); + // 可以尝试设置其他窗口属性 + } +// WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); +// layoutParams.width = 1550; +//// layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; +// getWindow().setAttributes(layoutParams); + } + + @Override + protected void onViewInit() { + super.onViewInit(); + dataView = findViewById(R.id.data); + titleView = findViewById(R.id.title_view); + btnCancel = findViewById(R.id.btn_cancel); + Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), "courbd.ttf");//等宽的字体,避免显示混乱(微软字体,目前查到的是免费试用) + dataView.setTypeface(typeface); + if (data.endsWith("\n")) { + data = data.trim(); + } + dataView.setText(data); + titleView.setText(title); + } + + @Override + protected void setOnListener() { + super.setOnListener(); + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/StartupConfigDialog.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/StartupConfigDialog.java new file mode 100644 index 0000000000..8557e5ad34 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/StartupConfigDialog.java @@ -0,0 +1,157 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog; + +import android.content.Context; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; + +import com.mogo.eagle.core.utilcode.util.SizeUtils; +import com.mogo.eagle.core.utilcode.util.ToastUtils; +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseDialog; +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.Utils; +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey; +import com.zhjt.mogo_core_function_devatools.rviz.dialog.adapter.StartupConfigAdapter; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerConfigContent; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.StartupConfig; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.constant.MogoCommand; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; +import com.zhjt.mogo_core_function_devatools.rviz.widgets.MyLinearLayoutManager; + +/** + * 配置文件展示 + */ +public class StartupConfigDialog extends BaseDialog implements LifecycleOwner { + private final StartupConfig config; + private final String title; + private OnStartupConfigListener listener; + private final LifecycleRegistry lifecycle = new LifecycleRegistry(this); + private StartupConfigAdapter adapter; + private RecyclerView dockerListView; + private TextView headerStart; + private TextView headerEnd; + private TextView titleView; + private Button btnCancel; + + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + + public interface OnStartupConfigListener { + void onQuery(SSHHostBean host, String path); + + void onDismiss(SSHHostBean host); + } + + public StartupConfigDialog(Context context, String title, StartupConfig config, OnStartupConfigListener listener) { + super(context); + this.title = title; + this.config = config; + this.listener = listener; + } + + @Override + protected View getContentViewResource() { + return getLayoutInflater().inflate(R.layout.rviz_fmd_dialog_dockers, null, false); + } + + @Override + protected void onViewInitBefore() { + super.onViewInitBefore(); + lifecycle.setCurrentState(Lifecycle.State.CREATED); + getWindow().setBackgroundDrawableResource(R.drawable.rviz_common_bg_dialog); + WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); + layoutParams.width = SizeUtils.dp2px(getContext().getResources().getDimension(R.dimen.dp_600)); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + getWindow().setAttributes(layoutParams); + } + + @Override + protected void onViewInit() { + super.onViewInit(); + dockerListView = findViewById(R.id.docker_list_view); + headerStart = findViewById(R.id.header_start); + headerEnd = findViewById(R.id.header_end); + titleView = findViewById(R.id.title_view); + btnCancel = findViewById(R.id.btn_cancel); + + titleView.setText(title); + headerStart.setGravity(Gravity.CENTER_VERTICAL); + headerStart.setText("配置路径"); + headerEnd.setVisibility(View.INVISIBLE); + RecyclerView.ItemAnimator animator = dockerListView.getItemAnimator(); + if (animator instanceof SimpleItemAnimator) { + ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); + } + MyLinearLayoutManager linearLayoutManager = new MyLinearLayoutManager(getContext()); + linearLayoutManager.setOrientation(GridLayoutManager.VERTICAL); + dockerListView.setLayoutManager(linearLayoutManager); + adapter = new StartupConfigAdapter(config.configs, new StartupConfigAdapter.OnStartupConfigListener() { + @Override + public void onQuery(String path) { + if (listener != null) { + listener.onQuery(config.host, path); + } + } + }); + dockerListView.setAdapter(adapter); + } + + + @Override + protected void onViewCreated() { + super.onViewCreated(); + lifecycle.setCurrentState(Lifecycle.State.STARTED); + FlowBus.INSTANCE.with(EventKey.QUERY_DOCKER_CONFIG_CONTENT).register(this, it -> { + String path = it.cmd.replace(MogoCommand.QUERY_DOCKER_CONFIG_CONTENT, ""); + if (TextUtils.isEmpty(it.content) || it.content.contains("No such file or directory")) { + adapter.updateLoading(path, 2); + ToastUtils.showShort("文件不存在或打开失败"); + } else { + adapter.updateLoading(path, 1); + new ShowConfigDialog( + getContext(), + Utils.getIPLastSegment(it.host.getHostname()) + "--" + path, + it.content + ).show(); + } + }); + + } + + @Override + protected void setOnListener() { + super.setOnListener(); + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + } + + @Override + protected void onDismiss() { + super.onDismiss(); + lifecycle.setCurrentState(Lifecycle.State.DESTROYED); + if (listener != null) { + listener.onDismiss(config.host); + } + listener = null; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/DockerListAdapter.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/DockerListAdapter.java new file mode 100644 index 0000000000..8c009ad8c6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/DockerListAdapter.java @@ -0,0 +1,79 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog.adapter; + +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseAdapter; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseViewHolder; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerInfo; + +public class DockerListAdapter extends BaseAdapter { + + + @Override + protected void onBindDataToItem(MyViewHolder viewHolder, DockerInfo data, int position) { + viewHolder.itemView.setBackgroundResource(position % 2 == 0 ? R.drawable.rviz_fmd_bg_item_dockers_even : R.drawable.rviz_fmd_bg_item_dockers_odd); + viewHolder.nameView.setText(data.getNames()); + viewHolder.imageView.setText(data.getImage()); + String status = data.getStatus(); + int color = R.color.rviz_fmd_docker_status_not_running; + String temStatus = status; + if (TextUtils.isEmpty(status)) { + temStatus = "未知"; + } else { + status = status.toLowerCase(); + if (status.startsWith("created")) { + temStatus = "已创建"; + } else if (status.startsWith("exited")) { + temStatus = "已停止"; + } else if (status.startsWith("paused")) { + temStatus = "已暂停"; + } else if (status.startsWith("restarting")) { + temStatus = "正在重启"; + } else if (status.startsWith("dead")) { + temStatus = "已停止(未清理)"; + } else if (status.startsWith("removing")) { + temStatus = "正在删除"; + } else if (status.startsWith("up") || status.startsWith("running")) { +// temStatus = temStatus.replace("Up", "正在运行"); + color = android.R.color.black; + } else { + temStatus = "未知"; + } + } + viewHolder.statusView.setText(temStatus); + int c = mContext.getColor(color); + viewHolder.statusView.setTextColor(c); + viewHolder.nameView.setTextColor(c); + viewHolder.imageView.setTextColor(c); + } + + @Override + protected View getItemViewResource(ViewGroup viewGroup) { + return LayoutInflater.from(mContext).inflate(R.layout.rviz_fmd_item_dockers, viewGroup, false); + } + + @Override + protected MyViewHolder getViewHolder(View view) { + return new MyViewHolder(view, this); + } + + //继承RecyclerView.ViewHolder抽象类的自定义ViewHolder + static class MyViewHolder extends BaseViewHolder { + TextView nameView; + TextView imageView; + TextView statusView; + + public MyViewHolder(View itemView, DockerListAdapter adapter) { + super(itemView, adapter); + nameView = itemView.findViewById(R.id.name_view); + imageView = itemView.findViewById(R.id.image_view); + statusView = itemView.findViewById(R.id.status_view); + } + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/FaultCodeDetailsAdapter.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/FaultCodeDetailsAdapter.kt new file mode 100644 index 0000000000..7809a5af30 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/FaultCodeDetailsAdapter.kt @@ -0,0 +1,153 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseExpandableListAdapter +import android.widget.TextView +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultLevel +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmCodeEntity +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * FM详细信息展示 + */ +class FaultCodeDetailsAdapter(val context: Context) : BaseExpandableListAdapter() { + val dataFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) + private var detailsInfos: ArrayList>>? = null + private var mContext: Context? = null + + init { + mContext = context + } + + fun setData(list: ArrayList>>) { + detailsInfos = list + notifyDataSetChanged() + } + + override fun getGroupCount(): Int { + return if (detailsInfos != null) detailsInfos!!.size else 0 + } + + override fun getChildrenCount(groupPosition: Int): Int { + return if (detailsInfos != null) detailsInfos!![groupPosition].second.size else 0 + } + + override fun getGroup(groupPosition: Int): Pair> { + return detailsInfos!![groupPosition] + } + + override fun getChild(groupPosition: Int, childPosition: Int): FmCodeEntity { + return detailsInfos!![groupPosition].second[childPosition] + } + + override fun getGroupId(groupPosition: Int): Long { + return groupPosition.toLong() + } + + override fun getChildId(groupPosition: Int, childPosition: Int): Long { + return childPosition.toLong() + } + + override fun hasStableIds(): Boolean { + return false + } + + override fun getGroupView( + groupPosition: Int, + isExpanded: Boolean, + convertView: View?, + parent: ViewGroup? + ): View { + val view = convertView ?: LayoutInflater.from(context) + .inflate(R.layout.rviz_fmd_item_group_fault_code_details, parent, false) + val tvGroupTitle = view.findViewById(R.id.tvGroupTitle) + val tvGroupSize = view.findViewById(R.id.tvGroupSize) + val data = getGroup(groupPosition) + tvGroupTitle.text = data.first + tvGroupSize.text = String.format("%d个故障", data.second.size) + return view + } + + override fun getChildView( + groupPosition: Int, + childPosition: Int, + isLastChild: Boolean, + convertView: View?, + parent: ViewGroup? + ): View { + val view = convertView ?: LayoutInflater.from(context) + .inflate(R.layout.rviz_fmd_item_child_fault_code_details, parent, false) + val tvTimeValue = view.findViewById(R.id.tvTimeValue) + val tvCodeValue = view.findViewById(R.id.tvCodeValue) + val tvSubModuleValue = view.findViewById(R.id.tvSubModuleValue) + val tvNameValue = view.findViewById(R.id.tvNameValue) + val tvLevelValue = view.findViewById(R.id.tvLevelValue) + val tvCauseValue = view.findViewById(R.id.tvCauseValue) + val tvInfluenceValue = view.findViewById(R.id.tvInfluenceValue) + val tvSuggestValue = view.findViewById(R.id.tvSuggestValue) + val data = getChild(groupPosition, childPosition) + val p = FaultLevel.getMessageAndColor(data.systemDegradationStrategy, data.faultLevel) + val color = context.getColor(p.second) + tvTimeValue.setTextColor(color) + tvCodeValue.setTextColor(color) + tvSubModuleValue.setTextColor(color) + tvNameValue.setTextColor(color) + tvLevelValue.setTextColor(color) + tvCauseValue.setTextColor(color) + tvInfluenceValue.setTextColor(color) + tvSuggestValue.setTextColor(color) + //上报时间 + tvTimeValue.text = dataFormat.format(Date(data.faultTime)) + //降级策略 + tvLevelValue.text = p.first + + //故障码 + tvCodeValue.text = data.faultCode.ifEmpty { + "未知" + } + tvSubModuleValue.text = if (data.subModuleName.isNullOrEmpty()) { + "未知" + } else { + data.subModuleName + } + //故障名称 + tvNameValue.text = if (data.faultName.isNullOrEmpty()) { + "未知" + } else { + data.faultName + } + + //故障原因 + tvCauseValue.text = if (data.faultReason.isNullOrEmpty()) { + "暂无" + } else { + data.faultReason + } + //系统影响 + tvInfluenceValue.text = if (data.systemImpact.isNullOrEmpty()) { + "暂无" + } else { + data.systemImpact + } + //处理建议 + tvSuggestValue.text = if (data.troubleshootingSuggestions.isNullOrEmpty()) { + "暂无" + } else { + data.troubleshootingSuggestions + } + return view + } + + + override fun isChildSelectable(p0: Int, p1: Int): Boolean { + return true + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/StartupConfigAdapter.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/StartupConfigAdapter.java new file mode 100644 index 0000000000..d193537ed0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/dialog/adapter/StartupConfigAdapter.java @@ -0,0 +1,119 @@ +package com.zhjt.mogo_core_function_devatools.rviz.dialog.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseAdapter; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseViewHolder; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.StartupConfig; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class StartupConfigAdapter extends BaseAdapter { + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + private final OnStartupConfigListener listener; + + public StartupConfigAdapter(List configs, OnStartupConfigListener listener) { + super(configs); + this.listener = listener; + } + + + public interface OnStartupConfigListener { + void onQuery(String path); + } + + public void updateLoading(String path, int catState) { + int index = mDatas.indexOf(new StartupConfig.Config(path, null)); + if (index > -1) { + StartupConfig.Config config = mDatas.get(index); + config.isShowLoading = false; + config.catState = catState; + notifyItemChanged(index); + } + } + + + @Override + protected void onBindDataToItem(MyViewHolder viewHolder, StartupConfig.Config config, int position) { + viewHolder.itemView.setBackgroundResource(position % 2 == 0 ? R.drawable.rviz_fmd_bg_item_dockers_even : R.drawable.rviz_fmd_bg_item_dockers_odd); + viewHolder.nameView.setText(config.path); + viewHolder.publishTimeView.setText("发布时间:" + sdf.format(new Date(config.attribute.getPublish_timestamp() * 1000L))); + viewHolder.updateTimeView.setText("本地更新时间:" + sdf.format(new Date(config.attribute.getModify_timestamp() * 1000L))); + viewHolder.loading.setVisibility(config.isShowLoading ? View.VISIBLE : View.GONE); + viewHolder.btnLook.setVisibility(!config.isShowLoading ? View.VISIBLE : View.INVISIBLE); + viewHolder.updateTextColor(config.catState); + } + + @Override + protected View getItemViewResource(ViewGroup viewGroup) { + return LayoutInflater.from(mContext).inflate(R.layout.rviz_fmd_item_startup_config, viewGroup, false); + } + + @Override + protected MyViewHolder getViewHolder(View view) { + return new MyViewHolder(view, this); + } + + //继承RecyclerView.ViewHolder抽象类的自定义ViewHolder + class MyViewHolder extends BaseViewHolder { + TextView nameView; + TextView publishTimeView; + TextView updateTimeView; + TextView btnLook; + ProgressBar loading; + + public MyViewHolder(View itemView, StartupConfigAdapter adapter) { + super(itemView, adapter); + nameView = itemView.findViewById(R.id.name_view); + publishTimeView = itemView.findViewById(R.id.publish_time_view); + updateTimeView = itemView.findViewById(R.id.update_time_view); + btnLook = itemView.findViewById(R.id.btn_look); + loading = itemView.findViewById(R.id.loading); + nameView.setSelected(true); + btnLook.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (listener != null) { + int pos = getBindingAdapterPosition(); + StartupConfig.Config config = mDatas.get(pos); + config.isShowLoading = true; + if (config.catState == 0) { + config.catState = 1; + updateTextColor(config.catState); + } + notifyItemChanged(pos); + listener.onQuery(config.path); + } + } + }); + } + + /** + * 更新文字颜色 + */ + public void updateTextColor(int catState) { + int tem; + if (catState == 1) { + tem = R.color.rviz_fmd_clock_look; + } else if (catState == 2) { + tem = R.color.rviz_fmd_docker_status_not_running; + } else { + tem = android.R.color.black; + } + int color = mContext.getColor(tem); + nameView.setTextColor(color); + publishTimeView.setTextColor(color); + updateTimeView.setTextColor(color); + } + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/CarStatusDao.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/CarStatusDao.java new file mode 100644 index 0000000000..20db70f3dc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/CarStatusDao.java @@ -0,0 +1,15 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.db; + +import androidx.room.Dao; + +import com.zhjt.mogo_core_function_devatools.rviz.common.db.BaseDao; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.CarStatusEntity; + +@Dao +public abstract class CarStatusDao implements BaseDao { + + static final String TAG = CarStatusDao.class.getCanonicalName(); + + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/DataStorage.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/DataStorage.java new file mode 100644 index 0000000000..65f616dfe7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/DataStorage.java @@ -0,0 +1,63 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.db; + +import android.content.Context; +import android.util.Log; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.LambdaTask; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmCodeEntity; + +import java.util.List; + + +@Database(entities = {FmCodeEntity.class}, + version = 1, exportSchema = false) +public abstract class DataStorage extends RoomDatabase { + + private static String DB_NAME = "AutoPilotVisualDB.db"; + + private static final String TAG = DataStorage.class.getCanonicalName(); + private static DataStorage instance; + + public static synchronized DataStorage getInstance(final Context context) { + if (instance == null) { + instance = Room.databaseBuilder(context.getApplicationContext(), DataStorage.class, DB_NAME) + .allowMainThreadQueries() + .build(); + } + return instance; + } + + + // DAO Methods --------------------------------------------------------------------------------- + + public abstract FmCodeDao fmCodeDao(); + + + // Config methods ------------------------------------------------------------------------------ + public void addFmCode(FmCodeEntity fmCode) { + new LambdaTask(() -> { + long result = fmCodeDao().insert(fmCode); + Log.d("DataStorage", "插入数据result=" + result); + }).execute(); + } + + public void updateFmCode(FmCodeEntity fmCode) { + new LambdaTask(() -> fmCodeDao().update(fmCode)).execute(); + } + + public void deleteFmCode(FmCodeEntity config) { + new LambdaTask(() -> fmCodeDao().delete(config)).execute(); + } + + public List getAllFmCode() { + return fmCodeDao().getAllFmCode(); + } + + public FmCodeEntity getFmCodeInfo(String faultCode) { + return fmCodeDao().getFmCodeInfo(faultCode); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeDao.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeDao.java new file mode 100644 index 0000000000..a1df2abc10 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeDao.java @@ -0,0 +1,33 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.db; + +import androidx.room.Dao; +import androidx.room.Query; + +import com.zhjt.mogo_core_function_devatools.rviz.common.db.BaseDao; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmCodeEntity; + +import java.util.List; + +/** + * 故障码数据路操作 + */ +@Dao +public abstract class FmCodeDao implements BaseDao { + + static final String TAG = FmCodeDao.class.getCanonicalName(); + + @Query("SELECT * FROM fm_code_table") + abstract List getAllFmCode(); + + + /** + * 查询指定的 faultCode 对应的故障信息 + * + * @param faultCode 故障码 + * @return 故障信息 + */ + @Query("SELECT * FROM fm_code_table WHERE faultCode = :faultCode") + abstract FmCodeEntity getFmCodeInfo(String faultCode); + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeRepository.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeRepository.java new file mode 100644 index 0000000000..f8478593bc --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/db/FmCodeRepository.java @@ -0,0 +1,30 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.db; + +import com.mogo.commons.AbsMogoApplication; +import com.mogo.eagle.core.utilcode.util.ActivityUtils; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmCodeEntity; + +import java.util.List; + +/** + * 故障码数据库操作 + */ +public class FmCodeRepository { + + static DataStorage mDataStorage; + + public static List getAllList() { + if (mDataStorage == null) { + mDataStorage = DataStorage.getInstance(AbsMogoApplication.getApp()); + } + return mDataStorage.getAllFmCode(); + } + + public static FmCodeEntity getFmCodeInfo(String faultCode) { + if (mDataStorage == null) { + mDataStorage = DataStorage.getInstance(AbsMogoApplication.getApp()); + } + return mDataStorage.getFmCodeInfo(faultCode); + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/AdasConnectionStatus.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/AdasConnectionStatus.kt new file mode 100644 index 0000000000..d62b042520 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/AdasConnectionStatus.kt @@ -0,0 +1,5 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +import com.zhjt.mogo.adas.data.AdasConstants + +data class AdasConnectionStatus(val ipcConnectionStatus: AdasConstants.IpcConnectionStatus, val reason: String?) diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/CarStatusEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/CarStatusEntity.java new file mode 100644 index 0000000000..e7e49b0427 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/CarStatusEntity.java @@ -0,0 +1,28 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * 聚合车辆状态,数据库操作 + */ +@Entity(tableName = "car_status_table") +public class CarStatusEntity { + + // 数据库主键 + @PrimaryKey(autoGenerate = true) + public long id; + + public long time; + + public String mapVersion; + public String hdMapVersion; + public String eagleEyeVersionName; + public int eagleEyeVersionCode; + + public HashMap> sensorStatusEntities = new HashMap<>(); + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiagnoseInfo.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiagnoseInfo.kt new file mode 100644 index 0000000000..b4a2f9be2e --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiagnoseInfo.kt @@ -0,0 +1,24 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +import com.zhjt.mogo_core_function_devatools.rviz.constant.DiagnoseSource +import com.zhjt.mogo_core_function_devatools.rviz.constant.DiagnoseType + +data class DiagnoseInfo( + val time: Long,//时间 + val source: DiagnoseSource,//来源 + val msg: String?,//消息 + val type: DiagnoseType = DiagnoseType.NORMAL,//消息类型 + val isAdd: Boolean = true// true:新增 false:刷新原有的数据 +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as DiagnoseInfo + if (msg != other.msg) return false + return true + } + + override fun hashCode(): Int { + return msg?.hashCode() ?: 0 + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiskInfo.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiskInfo.java new file mode 100644 index 0000000000..591d3244f3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DiskInfo.java @@ -0,0 +1,13 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +public class DiskInfo { + public final SSHHostBean host; + public final String data; + + public DiskInfo(SSHHostBean host, String data) { + this.host = host; + this.data = data; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerBean.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerBean.java new file mode 100644 index 0000000000..7344671393 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerBean.java @@ -0,0 +1,15 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +import java.util.ArrayList; + +public class DockerBean { + public final SSHHostBean host; + public final ArrayList dockers; + + public DockerBean(SSHHostBean host, ArrayList dockers) { + this.host = host; + this.dockers = dockers; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerConfigContent.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerConfigContent.java new file mode 100644 index 0000000000..8c52f72801 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerConfigContent.java @@ -0,0 +1,15 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +public class DockerConfigContent { + public final SSHHostBean host; + public final String cmd; + public final String content; + + public DockerConfigContent(SSHHostBean host, String cmd, String content) { + this.host = host; + this.cmd = cmd; + this.content = content; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerInfo.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerInfo.java new file mode 100644 index 0000000000..fcd7ae4535 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerInfo.java @@ -0,0 +1,184 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.google.gson.annotations.SerializedName; + +public class DockerInfo { + + /** + * Command : "/bin/sh /entrypoint…" + * CreatedAt : 2023-07-28 12:38:04 +0800 CST + * ID : 3dae431f0704 + * Image : ghcr.io/foxglove/studio:latest + * Labels : org.opencontainers.image.vendor=Light Code Labs,org.opencontainers.image.created=2023-07-25T19:09:10.689Z,org.opencontainers.image.description=Robotics visualization and debugging,org.opencontainers.image.source=https://github.com/foxglove/studio,org.opencontainers.image.title=studio,org.opencontainers.image.version=1.63.0,org.opencontainers.image.documentation=https://caddyserver.com/docs,org.opencontainers.image.licenses=MPL-2.0,org.opencontainers.image.revision=235048670f5f61254bbdfa623534e0ff366feb09,org.opencontainers.image.url=https://github.com/foxglove/studio + * LocalVolumes : 0 + * Mounts : /data/autocar,/dev + * Names : foxglove_studio + * Networks : host + * Ports : + * RunningFor : 3 months ago + * Size : 10.4kB (virtual 159MB) + * State : running + * Status : Up 20 minutes + */ + + @SerializedName("Command") + private String command; + @SerializedName("CreatedAt") + private String createdAt; + @SerializedName("ID") + private String id; + @SerializedName("Image") + private String image; + @SerializedName("Labels") + private String labels; + @SerializedName("LocalVolumes") + private String localVolumes; + @SerializedName("Mounts") + private String mounts; + @SerializedName("Names") + private String names; + @SerializedName("Networks") + private String networks; + @SerializedName("Ports") + private String ports; + @SerializedName("RunningFor") + private String runningFor; + @SerializedName("Size") + private String size; + @SerializedName("State") + private String state; + @SerializedName("Status") + private String status; + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public String getLabels() { + return labels; + } + + public void setLabels(String labels) { + this.labels = labels; + } + + public String getLocalVolumes() { + return localVolumes; + } + + public void setLocalVolumes(String localVolumes) { + this.localVolumes = localVolumes; + } + + public String getMounts() { + return mounts; + } + + public void setMounts(String mounts) { + this.mounts = mounts; + } + + public String getNames() { + return names; + } + + public void setNames(String names) { + this.names = names; + } + + public String getNetworks() { + return networks; + } + + public void setNetworks(String networks) { + this.networks = networks; + } + + public String getPorts() { + return ports; + } + + public void setPorts(String ports) { + this.ports = ports; + } + + public String getRunningFor() { + return runningFor; + } + + public void setRunningFor(String runningFor) { + this.runningFor = runningFor; + } + + public String getSize() { + return size; + } + + public void setSize(String size) { + this.size = size; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String toString() { + return "DockerInfo{" + + "command='" + command + '\'' + + ", createdAt='" + createdAt + '\'' + + ", id='" + id + '\'' + + ", image='" + image + '\'' + + ", labels='" + labels + '\'' + + ", localVolumes='" + localVolumes + '\'' + + ", mounts='" + mounts + '\'' + + ", names='" + names + '\'' + + ", networks='" + networks + '\'' + + ", ports='" + ports + '\'' + + ", runningFor='" + runningFor + '\'' + + ", size='" + size + '\'' + + ", state='" + state + '\'' + + ", status='" + status + '\'' + + '}'; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerStatus.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerStatus.java new file mode 100644 index 0000000000..fe120353e6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/DockerStatus.java @@ -0,0 +1,13 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +public class DockerStatus { + public final SSHHostBean host; + public final int status; + + public DockerStatus(SSHHostBean host, int status) { + this.host = host; + this.status = status; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FMInfoMsg.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FMInfoMsg.kt new file mode 100644 index 0000000000..e8e6926ae6 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FMInfoMsg.kt @@ -0,0 +1,22 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +import fault_management.FmInfo +import java.io.Serializable + +/** + * FM数据的类型 + */ +data class FMInfoMsg( + var fmInfoList: List?, + var policyCode: String?, + var policyTime: Long? +) : Serializable + +/** + * 数据中心使用,用于过滤变更数据 + */ +data class FMFilterInfoMsg( + var fmInfoList: List?, + var policyCode: String?, + var cacheFilterList: MutableList? +) \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeDetailsInfo.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeDetailsInfo.kt new file mode 100644 index 0000000000..cc27b92136 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeDetailsInfo.kt @@ -0,0 +1,5 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +import fault_management.FmInfo + +data class FaultCodeDetailsInfo(val name: String, val data: ArrayList) diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeEntity.java new file mode 100644 index 0000000000..2a145ab5b9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FaultCodeEntity.java @@ -0,0 +1,28 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +/** + * 数据库操作 + * 节点健康 + */ +@Entity(tableName = "fault_code_table") +public class FaultCodeEntity { + + // 数据库主键 + @PrimaryKey(autoGenerate = true) + public long id; + + public long time;//存入时间 + + public String fault_id; //故障标识,每个告警有一个全域唯一的标识 + public long fault_time; //故障确认上报时间,毫秒单位 + public String fault_desc; //故障的补充描述,由模块补充上报信息,上报段自定义 + public String fault_level; //原有预留字段,故障上报不需要填,故障状态发布时由fm根据配置填入 故障健康等级 + public String fault_action; //新增故障处理方法,故障上报不需要填,故障状态发布时由fm根据配置填入, 用于故障上报给pad 云控处理, 需要和产品,pad端沟通定义,罗列定义值 + public String fault_result; //新增故障后果,故障上报不需要填,故障状态发布时由fm根据配置表填入, 用于行为上层的禁止 + public String policy_code; //新增故障策略,故障上报不需要填,故障表中所定义的故障策略 + public String fault_name; //新增故障名称,故障上报不需要填,故障表中定义的故障中文名称 + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmCodeEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmCodeEntity.java new file mode 100644 index 0000000000..acb40303de --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmCodeEntity.java @@ -0,0 +1,51 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + + +/** + * 对应的 + */ +@Entity(tableName = "fm_code_table", indices = {@Index(value = {"faultCode"}, unique = true)}) +public class FmCodeEntity { + + @Ignore + public long faultTime; + @Ignore + public String faultLevel; + @Ignore + public String originalData; + @Ignore + public String subModuleName; + + @PrimaryKey(autoGenerate = true) + public int id; + @NonNull + public String moduleId;//模块标识 + @NonNull + public String subModuleId;//子模块标识 + @NonNull + public String faultId;//故障标识 + @NonNull + public String faultCode;//故障码 + public String faultName;//故障名称 + public String faultConsequenceEffect;//故障后果影响 + public String faultHandlingBehavior;//故障处理行为 + public String systemDegradationStrategy;//系统降级策略 FM PB字段对应 policyCode + public String preFault;//前置故障 + public String faultReason;//故障可能原因 + public String subSystemLoss;//子系统功能损失 + public String systemImpact;//系统影响 + public String vehicleOperationScenario;//车辆运行场景 + public String troubleshootingSuggestions;//故障后处理措施建议 + public String faultDetectionConditions;//故障检测条件/策略 + public String faultRecoveryConditions;//故障恢复条件/策略 + public String faultLock;//故障锁存机制 + public String dataRecording;//数据录制需求 + public String dataSnapshot;//数据快照需求 + public String applicableVehicleType;//适用车型 +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmEntity.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmEntity.kt new file mode 100644 index 0000000000..47fab214ab --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/FmEntity.kt @@ -0,0 +1,12 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultModuleId +import fault_management.FmInfo + +data class FmEntity( + val title: String, + var stopFaultNum: Int = 0,//停车故障 等级3和4的和 + var otherFaultNum: Int = 0,//其他故障 其他等级的和 + var unknownFaultNum: Int = 0,//未知故障 未知故障等级的和(域控未赋值) + val data: ArrayList = arrayListOf(), +) diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/HdMapVersion.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/HdMapVersion.kt new file mode 100644 index 0000000000..00f3d62f01 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/HdMapVersion.kt @@ -0,0 +1,8 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +/** + * 高精地图版本 + */ +data class HdMapVersion(var ip: String = "192.168.1.10X", var version: String = "未知") { + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/ModuleStatusEntity.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/ModuleStatusEntity.kt new file mode 100644 index 0000000000..73bd1d991f --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/ModuleStatusEntity.kt @@ -0,0 +1,22 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +import com.zhjt.mogo_core_function_devatools.rviz.R + + +/** + * 传感器状态 + */ +data class ModuleStatusEntity( + // 模块标题 + var title: String, + + // 模块图标 + var sensorBg: Int = R.drawable.rviz_fmd_icon_ipc, + + // 是否是日志信息,true--是日志,false--不是日志 + // 如果是日志信息按照垂直列表的展示,如果不是日志,则按照3列的网格排布 + var isLog: Boolean = false, + + // 当前模块传感器列表 + var sensorList: ArrayList = arrayListOf() +) \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/NodeHealthEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/NodeHealthEntity.java new file mode 100644 index 0000000000..163d62daa0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/NodeHealthEntity.java @@ -0,0 +1,25 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +/** + * 数据库操作 + * 节点健康 + */ +@Entity(tableName = "node_health_table") +public class NodeHealthEntity { + + // 数据库主键 + @PrimaryKey(autoGenerate = true) + public long id; + + public long time;//存入时间 + + public String agent_ip;//主机ip + public String launch_name;//启动 node 的 launch 服务名称 + public String node_path;//node 名称 + + public int state;//运行状态,备注:节点状态有:0-未启动,1-等待前置条件,2-启动中,3-运行态,4-停止态 5-无法启动 6-人为启动中,7-人为关闭中 + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/RosHostArgument.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/RosHostArgument.java new file mode 100644 index 0000000000..d48679d1f9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/RosHostArgument.java @@ -0,0 +1,276 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import android.graphics.Color; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +import java.util.Objects; + +import kotlin.Pair; + +public class RosHostArgument { + private final String regex = "[^0-9.]+"; + private final ForegroundColorSpan span_gray = new ForegroundColorSpan(Color.GRAY); + private final ForegroundColorSpan span_black = new ForegroundColorSpan(Color.BLACK); + public final SSHHostBean host;//主机地址 + + private String dockerVersion = "未知"; + public boolean isConnectFailure = false;// true 连接失败 + public String connectFailureReason = "设备连接失败"; + private double cpuUsageRate = -1;//CPU使用率 + private String load = "未知";//负载 + + + private String memTotal = "未知";//查询总内存 + private String memUsed = "未知";//查询已用内存 + private String swapTotal = "未知";//查询总的交换分区容量 + private String swapUsed = "未知";//查询用户使用的交换分区容量 + private String diskData = "未知";//查询磁盘总大小 + private String diskDataUsed = "未知";//查询磁盘使用大小 + private String runTime = "未知";//查询运行时间 + + private boolean isRosMaster = false;//是不是 ROS Master + + + public RosHostArgument(SSHHostBean host) { + this.host = host; + } + + public String getDockerVersion() { + return dockerVersion; + } + + public void setDockerVersion(String dockerVersion) { + this.dockerVersion = dockerVersion; + } + + /** + * 内存使用率 + * + * @return + */ + public Pair getMemUsageRate() { + return getCharSequence(memUsed, memTotal); + } + + /** + * 交换使用率 + * + * @return + */ + public Pair getSwapUsageRate() { + return getCharSequence(swapUsed, swapTotal); + } + + /** + * CPU使用率 + * + * @return + */ + public double getCpuUsageRate() { + return cpuUsageRate; + } + + + public void setCpuUsageRate(String cpuUsageRate) { + if (!TextUtils.isEmpty(cpuUsageRate)) { + try { + this.cpuUsageRate = Double.parseDouble(cpuUsageRate); + } catch (Exception e) { + e.printStackTrace(); + this.cpuUsageRate = -1; + } + } + if (this.cpuUsageRate < 0) + this.cpuUsageRate = -1; + } + + /** + * 磁盘使用率 + * + * @return + */ + public Pair getDiskUsageRate() { + return getCharSequence(diskDataUsed, diskData); + } + + + public String getRunningTime() { + return runTime; + } + + + public String getLoad() { + return load; + } + + //使用HTML原因:SpannableString setSpan无法连续设置一个span + public void setLoad(String load) { + if (!TextUtils.isEmpty(load)) { + load = load.replace(", ", " | ").replace("\n", "").trim(); + this.load = load; + } else { + this.load = "未知"; + } + } + + public void setMemTotal(String memTotal) { + if (!TextUtils.isEmpty(memTotal)) { + this.memTotal = memTotal; + } else { + this.memTotal = "未知"; + } + } + + public void setMemUsed(String memUsed) { + if (!TextUtils.isEmpty(memUsed)) { + this.memUsed = memUsed; + } else { + this.memUsed = "未知"; + } + } + + public void setSwapTotal(String swapTotal) { + if (!TextUtils.isEmpty(swapTotal)) { + this.swapTotal = swapTotal; + } else { + this.swapTotal = "未知"; + } + } + + public void setSwapUsed(String swapUsed) { + if (!TextUtils.isEmpty(swapUsed)) { + this.swapUsed = swapUsed; + } else { + this.swapUsed = "未知"; + } + } + + public void setDiskData(String diskData) { + if (!TextUtils.isEmpty(diskData)) { + this.diskData = diskData; + } else { + this.diskData = "未知"; + } + } + + public void setDiskDataUsed(String diskDataUsed) { + if (!TextUtils.isEmpty(diskDataUsed)) { + this.diskDataUsed = diskDataUsed; + } else { + this.diskDataUsed = "未知"; + } + } + + public void setRunTime(String runTime) { + if (!TextUtils.isEmpty(runTime)) { + this.runTime = runTime; + } else { + this.runTime = "未知"; + } + } + + public boolean isRosMaster() { + return isRosMaster; + } + + public void setRosMaster(boolean rosMaster) { + isRosMaster = rosMaster; + } + + public void resetConnectFailureCode() { + isConnectFailure = false; + connectFailureReason = "设备连接失败"; + } + + public void initData() { + dockerVersion = "未知"; + cpuUsageRate = -1;//CPU使用率 + load = "未知";//负载 + memTotal = "未知";//查询总内存 + memUsed = "未知";//查询已用内存 + swapTotal = "未知";//查询总的交换分区容量 + swapUsed = "未知";//查询用户使用的交换分区容量 + diskData = "未知";//查询磁盘总大小 + diskDataUsed = "未知";//查询磁盘使用大小 + runTime = "未知";//查询运行时间 + isRosMaster = false; + } + + + private Pair getCharSequence(String used, String total) { + String str = used + "/" + total; + SpannableString spannableString = new SpannableString(str); + try { + if (!TextUtils.isEmpty(used) && !used.equals("未知") && !TextUtils.isEmpty(total) && !total.equals("未知")) { +// String temT = total.replaceAll(regex, ""); +// String temU = used.replaceAll(regex, ""); +// String unitT = total.replace(temT, ""); +// String unitU = used.replace(temU, ""); +// int t = 1; +// int u = 1; +// if (!TextUtils.equals(unitT, unitU)) { +// if (unitT.startsWith("T")) { +// t = 1024; +// } +// if (unitU.startsWith("T")) { +// u = 1024; +// } +// } +// double temTotal = Double.parseDouble(temT) * t; +// double temUsed = Double.parseDouble(temU) * u; + double temTotal = parseCapacity(total); + double temUsed = parseCapacity(used); + double percent = temUsed / temTotal * 100.0; + int index = str.indexOf('/'); + if (index != -1) { + spannableString.setSpan(span_gray, index, index + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return new Pair(percent, spannableString); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + spannableString.setSpan(span_black, 0, str.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return new Pair(-1.0, spannableString); + } + + private double parseCapacity(String capacityString) { + int multiplier = 1; + if (capacityString.contains("M")) { + multiplier = 1024; // MB to KB + } else if (capacityString.contains("G")) { + multiplier = 1024 * 1024; // GB to KB + } else if (capacityString.contains("T")) { + multiplier = 1024 * 1024 * 1024; // TB to KB + } + return Double.parseDouble(capacityString.replaceAll(regex, "")) * multiplier; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RosHostArgument that = (RosHostArgument) o; + return Objects.equals(host, that.host); + } + + @NonNull + @Override + public String toString() { + return "主机:" + host + + " 内存使用率:" + getMemUsageRate() + + " 交换使用率:" + getSwapUsageRate() + + " CPU使用率:" + getCpuUsageRate() + + " 磁盘使用率:" + getDiskUsageRate() + + " 运行时长:" + getRunningTime() + + " 负载:" + getLoad(); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SensorStatusEntity.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SensorStatusEntity.kt new file mode 100644 index 0000000000..b20a35876d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SensorStatusEntity.kt @@ -0,0 +1,13 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities + +/** + * 传感器状态 + */ +data class SensorStatusEntity( + // 传感器名称 + var sensorName: String = "", + + // 传感器是否正常,true--正常,false--不正常 + var sensorIsOk: Boolean = true, + var notOkColorRes: Int = -1 +) \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/StartupConfig.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/StartupConfig.java new file mode 100644 index 0000000000..866f381427 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/StartupConfig.java @@ -0,0 +1,75 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +import java.util.List; +import java.util.Objects; + +public class StartupConfig { + public final SSHHostBean host; + public final List configs; + + + public StartupConfig(SSHHostBean host, List configs) { + this.host = host; + this.configs = configs; + } + + + public static class Config { + public boolean isShowLoading = false;//是否显示loading + public int catState = 0;//0:未查看,未点击 1:已查看,并且数据加载成功 2:已查看,并且数据加载失败 + public final String path;//配置所在路径 + public final Attribute attribute; + + public Config(String path, Attribute attribute) { + this.path = path; + this.attribute = attribute; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Config config = (Config) o; + return Objects.equals(path, config.path); + } + } + + public static class Attribute { + + /** + * url : https://map-algorithm-huadong-1255510688.cos.ap-beijing.myqcloud.com/data/HQHYD81J31/perception/radar/perception_radar.launch_1652244003428 + * modify_timestamp : 1683256531 + * id : /HQ/HQHYD1857L/perception/radar/perception_radar.launch + * publish_timestamp : 1660492800 + * md5 : 1103797f787680882c03fe1a243cde0a + */ + + private String url; + private int modify_timestamp; + private String id; + private int publish_timestamp; + private String md5; + + public String getUrl() { + return url; + } + + public int getModify_timestamp() { + return modify_timestamp; + } + + public String getId() { + return id; + } + + public int getPublish_timestamp() { + return publish_timestamp; + } + + public String getMd5() { + return md5; + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemLogEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemLogEntity.java new file mode 100644 index 0000000000..bf304d1e3a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemLogEntity.java @@ -0,0 +1,26 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +/** + * 数据库操作 + * 节点健康 + */ +@Entity(tableName = "system_log_table") +public class SystemLogEntity { + + // 数据库主键 + @PrimaryKey(autoGenerate = true) + public long id; + + public long time;//存入时间 + + public String src; //消息来源 + public String level; //error info + public String msg; //研发自己看的信息;对标准日志来说就是日志内容 + public String code; //error日志中的错误原因,这是一个类似宏的受约束字段,用字符串的目的是便于排查问题时查看 + public String result; //带来的后果;例如pad无法启动驾驶,远程驾驶无法启动等;可供监控后台做错误分类;pad无法理解code时也可参考此字段 + public String actions;//试验性字段。消息发出者希望触发的动作,例如:触发短信报警,自动创建工单,要求pad弹框等 + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemResourceEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemResourceEntity.java new file mode 100644 index 0000000000..4c985d51b1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/SystemResourceEntity.java @@ -0,0 +1,28 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +/** + * 数据库操作 + * 系统资源记录 + */ +@Entity(tableName = "system_resource_table") +public class SystemResourceEntity { + + // 数据库主键 + @PrimaryKey(autoGenerate = true) + public long id; + + public long time;//存入时间 + + public String ip;//主机ip + public String dockerMapVersion;//自动驾驶map版本 + public long runTime;//运行时间 + public long avg;//负载 + public long cpuUsage;//CPU占用 + public long memUsage;//内存占用 + public long swapUsage;//交换内存占用 + public long disksUsage;//磁盘占用 + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/TabEntity.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/TabEntity.java new file mode 100644 index 0000000000..ea0631daab --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/TabEntity.java @@ -0,0 +1,30 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import com.flyco.tablayout.listener.CustomTabEntity; + +public class TabEntity implements CustomTabEntity { + public String title; + public int selectedIcon; + public int unSelectedIcon; + + public TabEntity(String title, int selectedIcon, int unSelectedIcon) { + this.title = title; + this.selectedIcon = selectedIcon; + this.unSelectedIcon = unSelectedIcon; + } + + @Override + public String getTabTitle() { + return title; + } + + @Override + public int getTabSelectedIcon() { + return selectedIcon; + } + + @Override + public int getTabUnselectedIcon() { + return unSelectedIcon; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/VehicleConfig.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/VehicleConfig.java new file mode 100644 index 0000000000..763a3dbc7d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/model/entities/VehicleConfig.java @@ -0,0 +1,86 @@ +package com.zhjt.mogo_core_function_devatools.rviz.model.entities; + +import android.text.TextUtils; + +public class VehicleConfig { + private String plate = "未连接";//车牌 + private String brand = "未连接";//品牌 + private String model = "未连接";//类型 + + public VehicleConfig(String result) { + String[] lines = result.split("\n"); + for (String line : lines) { + if (!TextUtils.isEmpty(line)) { + String[] parts = line.trim().split(":"); + if (parts.length >= 2) { + String key = parts[0].trim(); + String value = parts[1].replace("\"", "").trim(); + if ("plate".equals(key)) { + plate = value; + model = getCarModel(value); + } else if ("brand".equals(key)) { + brand = getCarBrand(value); + } + + } + } + } + + } + + public String getPlate() { + return plate; + } + + public String getBrand() { + return brand; + } + + public String getModel() { + return model; + } + + /** + * 获取车辆品牌 + */ + private String getCarBrand(String brand) { + String data; + if (brand.startsWith("DF")) { + data = "东风"; + } else if (brand.startsWith("HQ")) { + data = "红旗"; + } else if (brand.startsWith("JINLV") || brand.startsWith("JV") || brand.startsWith("JL")) { + data = "金旅"; + } else if (brand.startsWith("KW")) { + data = "开沃"; + } else if (brand.startsWith("FT")) { + data = "福田"; + } else { + data = "金旅"; + } + + return data; + } + + /** + * 获取车辆品牌--类型 + */ + private String getCarModel(String plate) { + String data; + if (plate.startsWith("DF")) { + data = "E70"; + } else if (plate.startsWith("HQ")) { + data = "H9"; + } else if (plate.startsWith("JV") || plate.startsWith("JL")) { + data = "金旅牌XML6606JEVY0"; + } else if (plate.startsWith("KW")) { + data = "NJL6450ICEV"; + } else if (plate.startsWith("SW") || plate.startsWith("FT")) { + data = "清扫车"; + } else { + data = "金旅牌XML6606JEVY0"; + } + return data; + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/BaseResponse.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/BaseResponse.kt new file mode 100644 index 0000000000..5ec1478a9f --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/BaseResponse.kt @@ -0,0 +1,5 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net + +data class BaseResponse(val code: Int, val msg: String, val result: T?) + +data class Response(val code: Int, val msg: String, val data: T?) \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/FmdNetManager.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/FmdNetManager.kt new file mode 100644 index 0000000000..8a55c9dc55 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/FmdNetManager.kt @@ -0,0 +1,47 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net + +import com.zhjt.mogo_core_function_devatools.rviz.net.api.FMdNetModel +import com.zhjt.mogo_core_function_devatools.rviz.net.api.callback.CarInfoByParamCallback +import com.zhjt.mogo_core_function_devatools.rviz.net.api.entity.CarInfoByParamResponse +import kotlinx.coroutines.CoroutineScope + +class FmdNetManager private constructor() { + + private val fMdNetModel = FMdNetModel() + + /** + * 单利模式 + */ + companion object { + private val TAG = "LoginManager" + + val INSTANCE by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + FmdNetManager() + } + } + + /** + * 根据车牌获取城市获取地图信息 + */ + fun getCarInfoByParam( + scope: CoroutineScope, + carNum: String, + callback: CarInfoByParamCallback + ) { + fMdNetModel.getCarInfoByParam( + scope, + carNum, + object : NetworkCallback { + override fun onSuccess(data: CarInfoByParamResponse) { + callback.onSuccess(data) + } + + override fun onError(msg: String) { + callback.onError(msg) + } + } + ) + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/HostConst.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/HostConst.kt new file mode 100644 index 0000000000..431ccb94af --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/HostConst.kt @@ -0,0 +1,22 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net + +import com.mogo.commons.debug.DebugConfig + + +class HostConst { + + companion object { + private const val BI_HOST = "http://gateway.ee-private-dev1.myghost.zhidaoauto.com" + private const val BI_HOST_RELEASE = "https://mygateway.zhidaozhixing.com" + + + fun getBaseBiUrl(): String { + return if (DebugConfig.getNetMode() == DebugConfig.NET_MODE_QA) { + BI_HOST + } else { + BI_HOST_RELEASE + } + } + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/MoGoRetrofitFactory.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/MoGoRetrofitFactory.java new file mode 100644 index 0000000000..1eb0376065 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/MoGoRetrofitFactory.java @@ -0,0 +1,19 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net; + +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public final class MoGoRetrofitFactory { + + private MoGoRetrofitFactory() { + } + + public static synchronized Retrofit getInstanceNoCallAdapter(String baseUrl) { + return new Retrofit.Builder().baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/NetworkCallback.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/NetworkCallback.kt new file mode 100644 index 0000000000..2682e45552 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/NetworkCallback.kt @@ -0,0 +1,6 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net + +interface NetworkCallback { + fun onSuccess(data: T) + fun onError(msg:String) +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FMdNetModel.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FMdNetModel.kt new file mode 100644 index 0000000000..9369e1b0f5 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FMdNetModel.kt @@ -0,0 +1,73 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net.api + + +import android.util.Log +import com.zhjt.mogo_core_function_devatools.rviz.net.HostConst +import com.zhjt.mogo_core_function_devatools.rviz.net.MoGoRetrofitFactory +import com.zhjt.mogo_core_function_devatools.rviz.net.NetworkCallback +import com.zhjt.mogo_core_function_devatools.rviz.net.Response +import com.zhjt.mogo_core_function_devatools.rviz.net.api.entity.CarInfoByParamResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Callback + +class FMdNetModel { + private val TAG="FMdNetModel" + private val coroutineScopeMap = HashMap() + + private fun getNetWorkApi(baseUrl: String = HostConst.getBaseBiUrl()): FmdApi { + return MoGoRetrofitFactory.getInstanceNoCallAdapter(baseUrl) + .create(FmdApi::class.java) + } + + + + /** + * 根据车牌获取城市获取地图信息 + */ + fun getCarInfoByParam( + scope: CoroutineScope, + carNum: String, + callback: NetworkCallback + ) { + val call = getNetWorkApi().getCarInfoByParam(carNum, 1) + + coroutineScopeMap[scope]?.cancel() + val newJob = scope.async { + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: retrofit2.Response> + ) { + Log.i(TAG,"根据车牌获取城市获取地图信息:" + response.body().toString()) + scope.launch { + response.body()?.let { responseBody -> + responseBody.data?.let { data -> + callback.onSuccess(data) + } ?: let { + callback.onError("msg=${responseBody.msg},code=${responseBody.code}") + } + } ?: let { + callback.onError("网络异常") + } + } + } + + override fun onFailure(call: Call>, t: Throwable) { + Log.e(TAG, "网络异常", t) + scope.launch { callback.onError("网络异常") } + } + }) + } + coroutineScopeMap[scope] = newJob + + scope.coroutineContext[Job]?.invokeOnCompletion { + coroutineScopeMap.remove(scope) + } + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FmdApi.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FmdApi.kt new file mode 100644 index 0000000000..1baeb603f4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/FmdApi.kt @@ -0,0 +1,23 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net.api + +import com.zhjt.mogo_core_function_devatools.rviz.net.Response +import com.zhjt.mogo_core_function_devatools.rviz.net.api.entity.CarInfoByParamResponse +import retrofit2.Call +import retrofit2.http.* + + +interface FmdApi { + + /** + * 根据车牌获取城市获取地图信息 + * @param carNum 车牌 + * @param mapType 传1 + */ + @GET("/api/artifact/openApi/getCarInfoByParam") + fun getCarInfoByParam( + @Query("carNum") carNum: String, + @Query("mapType") mapType: Int + ): Call> + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/callback/CarInfoByParamCallback.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/callback/CarInfoByParamCallback.kt new file mode 100644 index 0000000000..662149d4ee --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/callback/CarInfoByParamCallback.kt @@ -0,0 +1,14 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net.api.callback + +import com.zhjt.mogo_core_function_devatools.rviz.net.api.entity.CarInfoByParamResponse + +/** + * 云端MAP版本 + */ +interface CarInfoByParamCallback { + + fun onSuccess(response: CarInfoByParamResponse) + + fun onError(error: String) + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/CarInfoByParamResponse.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/CarInfoByParamResponse.kt new file mode 100644 index 0000000000..36d548cad8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/CarInfoByParamResponse.kt @@ -0,0 +1,38 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net.api.entity + +/** + * 获取鹰眼扫码登陆状态返回实体类 + */ +data class CarInfoByParamResponse( + val carMapDownloadUrl: String?, + val carMapName: String?, + val carNum: String?, + val cityMapDownloadUrl: String?, + val cityMapName: String?, + val imageArtifactDto: ImageArtifactDto?, + val productId: Int +) + +data class ImageArtifactDto( + val bizType: String?, + val createTime: String?, + val createUser: String?, + val env: String?, + val extend: String?, + val fileAddr: String?, + val hashType: String?, + val hashValue: String?, + val id: Int?, + val iterationId: Int?, + val modelName: String?, + val packageType: Int?, + val pipelineArtifactId: Int?, + val pipelineResultId: Int?, + val product: String?, + val productName: String?, + val publishStage: String?, + val publishTime: String?, + val systemId: Int?, + val vehicleType: String?, + val versionNo: String? +) diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/Result.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/Result.kt new file mode 100644 index 0000000000..1af0be8d8e --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/net/api/entity/Result.kt @@ -0,0 +1,21 @@ +package com.zhjt.mogo_core_function_devatools.rviz.net.api.entity + +data class Result( + val applicationId: Int, + val applicationName: String, + val approvalId: Int, + val approvalStatus: Int, + val approvalTemplateId: Int, + val approvalTemplateName: String, + val approver: String, + val approverType: Int, + val createTime: String, + val createUser: String, + val currentNode: Int, + val custom: String, + val nodeName: String, + val previousNode: String, + val returnOut: String, + val submitMsg: Any, + val updateTime: String +) \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FaultManagementDiagnosisService.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FaultManagementDiagnosisService.kt new file mode 100644 index 0000000000..14a1b17177 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FaultManagementDiagnosisService.kt @@ -0,0 +1,1203 @@ +package com.zhjt.mogo_core_function_devatools.rviz.service + +import android.content.Context +import android.content.Intent +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.os.Message +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.text.TextUtils +import android.util.Log +import android.util.Pair +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotCarConfigListener +import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotStatusListener +import com.mogo.eagle.core.function.api.autopilot.IMoGoFaultManagementStateListener +import com.mogo.eagle.core.function.call.autopilot.CallerAutoPilotStatusListenerManager +import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotActionsListenerManager +import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotCarConfigListenerManager +import com.mogo.eagle.core.function.call.autopilot.CallerFaultManagementStateListenerManager +import com.mogo.eagle.core.utilcode.util.GsonUtils +import com.mogo.eagle.core.utilcode.util.ToastUtils +import com.zhjt.mogo.adas.data.AdasConstants +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseService +import com.zhjt.mogo_core_function_devatools.rviz.common.config.SSHAccountConfig +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.Utils +import com.zhjt.mogo_core_function_devatools.rviz.constant.AppConfigInfo +import com.zhjt.mogo_core_function_devatools.rviz.constant.DiagnoseSource +import com.zhjt.mogo_core_function_devatools.rviz.constant.DiagnoseType +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultLevel +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultModuleId +import com.zhjt.mogo_core_function_devatools.rviz.constant.SensorCamera +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.AdasConnectionStatus +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DiskInfo +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerBean +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerConfigContent +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerInfo +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerStatus +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FMFilterInfoMsg +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FMInfoMsg +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.HdMapVersion +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.RosHostArgument +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.SensorStatusEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.StartupConfig +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.VehicleConfig +import com.zhjt.mogo_core_function_devatools.rviz.net.FmdNetManager +import com.zhjt.mogo_core_function_devatools.rviz.net.api.callback.CarInfoByParamCallback +import com.zhjt.mogo_core_function_devatools.rviz.net.api.entity.CarInfoByParamResponse +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH +import com.zhjt.mogo_core_function_devatools.rviz.ssh.constant.MogoCommand +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.call.CallerSshConnectionListenerManager +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnDockerExecCommandListener +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnExecCommandListener +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnSshConnectionListener +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean +import fault_management.FmInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mogo.telematics.pad.MessagePad +import java.util.Locale +import java.util.Timer +import java.util.TimerTask +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicReference + +/** + * 故障管理诊断服务 + */ +class FaultManagementDiagnosisService : BaseService(), OnSshConnectionListener, + IMoGoAutopilotCarConfigListener, + IMoGoFaultManagementStateListener, OnExecCommandListener, + OnDockerExecCommandListener { + private val TAG: String = FaultManagementDiagnosisService::class.java.simpleName + private val WHAT_FM_INFO_TIMEOUT = 0//FM数据超时消息 + private val scopeSubscriber = CoroutineScope(Dispatchers.Main) + private val getCarInfoByParamScopeSubscriber = CoroutineScope(Dispatchers.IO) + private val binder: IBinder = FaultManagementDiagnosisBinder() + + + private val sshMap = ConcurrentHashMap()//已连接的SSH + private var defaultSSH: SSH? = null//默认SSH 一般是102 + private var vehicleConfig: VehicleConfig? = null//车辆信息 + public val rosHostArguments: ArrayList = + ArrayList() //每个ROS主机的配置信息 + private var rosHostArgumentTimer: AtomicReference = AtomicReference(null)//定时查询 + private val dockerInfoMap = mutableMapOf>() + private var cloudMapVersion = "未知"//云端MAP版本 + private var hdMapVersion = HdMapVersion()//高精度版本 + + private val queryRosHostArgumentMap = + ConcurrentHashMap()//当前参与查询主机参数的主机 + public val fmDataMap = mutableMapOf()//FM分类数据源 + private val cachePolicyMap = hashMapOf() + + fun getVehicleConfig(): VehicleConfig? { + return vehicleConfig + } + + fun getCloudMapVersion(): String { + return cloudMapVersion + } + + /** + * 获取102 MAP版本号 + */ + fun getRosMasterMapVersion(): String { + if (defaultSSH != null) { + val index = rosHostArguments.indexOf(RosHostArgument(defaultSSH!!.host)) + if (index > -1) { + val argument = rosHostArguments[index] + return argument.dockerVersion + } + } + return getString(R.string.rviz_fmd_unknown) + } + + /** + * 获取高精度图版本 + */ + fun getHdMapVersion(): HdMapVersion { + return hdMapVersion + + } + + override fun onCreate() { + super.onCreate() + initHandler(false)//初始化了个子线程Handler + CallerAutoPilotStatusListenerManager.addListener(TAG, adasConnectionStatuslistener) + CallerAutopilotCarConfigListenerManager.addListener(TAG, this) + CallerFaultManagementStateListenerManager.addListener(TAG, this) + initFMData() + Log.i(TAG, "故障管理诊断服务已启动") + } + + + override fun onBind(intent: Intent?): IBinder { + Log.i(TAG, "故障管理诊断服务已绑定") + return binder + } + + inner class FaultManagementDiagnosisBinder : Binder() { + val service: FaultManagementDiagnosisService + get() = this@FaultManagementDiagnosisService + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.i(TAG, "故障管理诊断服务已解绑") + return true + } + + override fun onDestroy() { + super.onDestroy() + + CallerFaultManagementStateListenerManager.removeListener(TAG) + CallerAutopilotCarConfigListenerManager.removeListener(TAG) + CallerAutoPilotStatusListenerManager.removeListener(TAG) + disconnectAllSSH() + defaultSSH = null + Log.i(TAG, "故障管理诊断服务已停止") + } + + private fun initFMData() { + fmDataMap[FaultModuleId.HardwareDriver.name] = + FmEntity("硬件驱动(HardwareDriver)") + fmDataMap[FaultModuleId.Perception.name] = + FmEntity("感知(Perception)") + fmDataMap[FaultModuleId.Localization.name] = + FmEntity("定位(Localization)") + fmDataMap[FaultModuleId.Prediction.name] = + FmEntity("预测(Prediction)") + fmDataMap[FaultModuleId.Planning.name] = + FmEntity("决策规划(Planning)") + fmDataMap[FaultModuleId.VehicleControl.name] = + FmEntity("车辆控制(VehicleControl)") + fmDataMap[FaultModuleId.SSM.name] = + FmEntity("系统管理(SSM)") + fmDataMap[FaultModuleId.SM.name] = FmEntity("系统监控(SM)") + fmDataMap[FaultModuleId.FSM.name + FaultModuleId.OTH.name] = + FmEntity("功能状态机&其他(FSM&OTH)") + } + + + /** + * 启动诊断 + */ + + fun startDiagnose() { + connectSSH() + } + + private val adasConnectionStatuslistener = object : IMoGoAutopilotStatusListener { + + override fun onAutopilotIpcConnectStatusChanged( + status: AdasConstants.IpcConnectionStatus, + reason: String? + ) { + super.onAutopilotIpcConnectStatusChanged(status, reason) + val adasConnectStatus = AdasConnectionStatus(status, reason) + when (status) { + AdasConstants.IpcConnectionStatus.DISCONNECTED -> { + + } + + AdasConstants.IpcConnectionStatus.CONNECTED -> { + + GlobalScope.launch(Dispatchers.IO) { + delay(6000) + updateFaultManagementStop(FMInfoMsg(null, null, null)) + } + } + + AdasConstants.IpcConnectionStatus.CONNECTING -> { + + } + + AdasConstants.IpcConnectionStatus.CONNECT_EXCEPTION -> { + } + + AdasConstants.IpcConnectionStatus.SEARCH_ADDRESS -> {} + AdasConstants.IpcConnectionStatus.NOT_FOUND_ADDRESS -> {} + else -> {} + } + FlowBus.with(EventKey.UPDATE_ADAS_CONNECT_STATE) + .post(scopeSubscriber, adasConnectStatus) + handler.sendEmptyMessage(WHAT_FM_INFO_TIMEOUT) + } + } + + + //连接SSH + private fun connectSSH() { + openConnection( + SSH.createHost( + SSHAccountConfig.getUserName(), + SSHAccountConfig.getPassWord(), + SSHAccountConfig.getRosMasterIp() + ) + ) + } + + private fun disconnectAllSSH() { + stopRosHostArgumentTimer() + rosHostArguments.clear() + hdMapVersion = HdMapVersion() + synchronized(sshMap) { + for (ssh in sshMap.values) { + ssh.close() + } + } + } + + + private fun updateFaultManagementStop(fmInfo: FMInfoMsg) { + //TODO +// if (isReceiveFMData.get()) { +// return +// } +// isReceiveFMData.set(true) + val msg: String + val type: DiagnoseType + if (fmInfo.fmInfoList != null) { + msg = "FM数据获取成功……" + type = DiagnoseType.SUCCEED + } else { + //证明未收到FM数据,存在两种情况,一个是没有发数据,一个是版本不支持 + FlowBus.with(EventKey.SEND_IS_SUPPORT_FM) + .post(scopeSubscriber, AppConfigInfo.isSupportFM) + if (AppConfigInfo.isSupportFM) { + msg = "FM数据获取成功……" + type = DiagnoseType.SUCCEED + } else { + msg = "获取FM数据失败,此MAP不支持FM相关功能" + type = DiagnoseType.FAILED + } + } + //TODO +// updateDiagnoseUIStateInUIThread( +// DiagnoseSource.ADAS, +// msg, +// type +// ) +// executeProcessConnectSSHStart() + } + + + //车辆基础信息 + @OptIn(DelicateCoroutinesApi::class) + override fun onAutopilotCarConfig(carConfigResp: MessagePad.CarConfigResp) { + Log.i(TAG, "当前域控版本=${carConfigResp.mapVersion}") + AppConfigInfo.dockerVersion = carConfigResp.dockVersion //工控机Docker版本 + AppConfigInfo.mapVersion = carConfigResp.mapVersion //工控机Docker版本 + AppConfigInfo.plateNumber = carConfigResp.plateNumber//车牌号 + AppConfigInfo.iPCMacAddress = carConfigResp.macAddress//工控机MAC地址 + AppConfigInfo.isSupportFM = carConfigResp.mapVersion >= 30600//支持FM + GlobalScope.launch(Dispatchers.IO) { + delay(100) + FlowBus.with(EventKey.UPDATE_CAR_CONFIG_STATE) + .post(scopeSubscriber, carConfigResp) + } + } + + override fun handleMessage(msg: Message) { + super.handleMessage(msg) + when (msg.what) { + WHAT_FM_INFO_TIMEOUT -> { + //接收消息超时,域控没有发FM数据认为是无异常 + FlowBus.with(EventKey.SEND_FM_INFO_TO_OVERVIEW_FRAGMENT) + .post(scopeSubscriber, FMInfoMsg(null, null, null)) + for (value in fmDataMap.values) { + value.stopFaultNum = 0; + value.otherFaultNum = 0; + value.unknownFaultNum = 0; + value.data.clear() + } + FlowBus.with(EventKey.UPDATE_FAULT_CODE_DATA) + .post(scopeSubscriber, -1) + } + } + } + + override fun onFaultManagementState(fmInfo: FmInfo.FaultResultMsg) { + Log.i(TAG, "FM原始数据个数=${fmInfo.infosList?.size}") + if (handler.hasMessages(WHAT_FM_INFO_TIMEOUT)) { + handler.removeMessages(WHAT_FM_INFO_TIMEOUT) + } + handler.sendEmptyMessageDelayed(WHAT_FM_INFO_TIMEOUT, 5000) + + val policyCode = fmInfo.downgradePolicyCode + if (policyCode == null || policyCode.isEmpty()) { + return + } + val list = fmInfo.infosList ?: return + //报告类数据不下发 + if ("FM_DP_NO_ACTION" == policyCode) { + // 故障清除 + if (cachePolicyMap.isNotEmpty()) { + cachePolicyMap.clear() + } + return + } + val fmFilterInfoMsg = cachePolicyMap[policyCode] + val cacheFaultList = ArrayList() + val policyTime = fmInfo.time + if (fmFilterInfoMsg?.cacheFilterList != null) { + if (fmFilterInfoMsg.cacheFilterList?.size == list.size) { + //判断两个集合重复 true:return + var sameResult = false + list.forEach { + sameResult = fmFilterInfoMsg.cacheFilterList?.contains(it.faultId) == true + } + if (sameResult) { + return + } + } + // 更新数据内容 + list.forEach { + cacheFaultList.add(it.faultId) + } + fmFilterInfoMsg.cacheFilterList?.clear() + fmFilterInfoMsg.cacheFilterList = cacheFaultList + fmFilterInfoMsg.fmInfoList = list + cachePolicyMap[policyCode] = fmFilterInfoMsg + onFaultManagementState(FMInfoMsg(list, policyCode, policyTime)) + } else { + // 首次添加 listener + cachePolicyMap.clear() + list.forEach { + cacheFaultList.add(it.faultId) + } + cachePolicyMap[policyCode] = FMFilterInfoMsg(list, policyCode, cacheFaultList) + onFaultManagementState(FMInfoMsg(list, policyCode, policyTime)) + } + } + + //相同数据过滤 + private fun onFaultManagementState(fmInfo: FMInfoMsg) { + if (fmInfo != null) { + Log.i(TAG, "FM数据变动个数=${fmInfo.fmInfoList?.size}") + updateFaultManagementStop(fmInfo) + FlowBus.with(EventKey.SEND_FM_INFO_TO_OVERVIEW_FRAGMENT) + .post(scopeSubscriber, fmInfo) + val fmInfoList = fmInfo.fmInfoList + for (value in fmDataMap.values) { + value.stopFaultNum = 0; + value.otherFaultNum = 0; + value.unknownFaultNum = 0; + value.data.clear() + } + if (!fmInfoList.isNullOrEmpty()) { + val set = mutableSetOf() + for (info in fmInfoList) { + var moduleId = info.faultId.substringBefore("_") + if (moduleId == "OTH" || moduleId == "FSM") { + moduleId = FaultModuleId.FSM.name + FaultModuleId.OTH.name + } + if (fmDataMap.containsKey(moduleId)) { + val tem = fmDataMap[moduleId]!! + if (!set.contains(moduleId)) { + set.add(moduleId) + tem.stopFaultNum = 0 + tem.otherFaultNum = 0 + tem.unknownFaultNum = 0 + tem.data.clear() + } + val isStop = FaultLevel.isStopFault(info.policyCode, info.faultLevel) + if (isStop != null) { + if (isStop) { + tem.stopFaultNum = + ++tem.stopFaultNum + } else { + tem.otherFaultNum = + ++tem.otherFaultNum + } + } + tem.data.add(info) + tem.unknownFaultNum = tem.data.size - tem.stopFaultNum - tem.otherFaultNum; + tem.data.sortWith(compareByDescending { FaultLevel.getOrder(it.policyCode) })//排序,等级高的显示在最上方 + } + } + } + FlowBus.with(EventKey.UPDATE_FAULT_CODE_DATA) + .post(scopeSubscriber, -1) + } + } + + + /** + * 获取云端车辆信息 + */ + private fun getCarInfoByParam(carNum: String) { + FmdNetManager.INSTANCE.getCarInfoByParam( + getCarInfoByParamScopeSubscriber, + carNum, + object : CarInfoByParamCallback { + override fun onSuccess(response: CarInfoByParamResponse) { + cloudMapVersion = + resources.getString(R.string.rviz_fmd_cloud_map_version_unknown) + val fileAddr = response.imageArtifactDto?.fileAddr + if (!fileAddr.isNullOrEmpty()) { + cloudMapVersion = if (fileAddr.contains(":")) { + val versionArray = fileAddr.split(":") + versionArray[1] + } else { + fileAddr + } + } + FlowBus.with(EventKey.SEND_CLOUD_MAP_VERSION) + .post(scopeSubscriber, cloudMapVersion) + Log.i(TAG, "云端MAP版本=${cloudMapVersion}") + } + + override fun onError(error: String) { + ToastUtils.showLong("云端版本号获取失败:${error}") + } + }) + } + + /**************************** SSH***************************************/ + + fun openConnection(host: SSHHostBean) { + // throw exception if terminal already open + val tem = getConnectedSSH(host) + if (tem != null && tem.isConnected) { + Log.i(TAG, "${host}连接已存在/建立") + return + } + val ssh = SSH(host) + ssh.setOnSshConnectionListener(this) + ssh.setExecCommandListener(this) + ssh.setExecDockerCommandListener(this) + ssh.connect() + } + + fun getConnectedSSH(host: SSHHostBean?): SSH? { + if (host == null) { + return null + } + return sshMap[host] + } + + /*********************SSH连接状态***************************/ + @OptIn(DelicateCoroutinesApi::class) + override fun onSshConnecting( + host: SSHHostBean, + rosHostArgumentPosition: Int, + isInserted: Boolean + ) { + + GlobalScope.launch(Dispatchers.Main) { + //TODO +// updateDiagnoseUIState( +// DiagnoseSource.SSH, +// "正在连接${Utils.getIPLastSegment(host.hostname)}……" +// ) + synchronized(rosHostArguments) { + val p = getRosHostArgument(host) + val argument = p.second + val isInsert = p.first < 0 + if (isInsert) {//不存在 + rosHostArguments.add(argument) + //根据IP排序,主动连接的主机(一般是rosMaster)置顶 + rosHostArguments.sortWith(Comparator { rosHostArgument1, rosHostArgument2 -> + if (defaultSSH != null) { + if (rosHostArgument1.host == defaultSSH!!.host) { + return@Comparator -1 + } + if (rosHostArgument2.host == defaultSSH!!.host) { + return@Comparator 1 + } + } + rosHostArgument1.host.hostname + .compareTo(rosHostArgument2.host.hostname) + }) + } else { + //已存在 + argument.resetConnectFailureCode() + } + if (TextUtils.equals(SSHAccountConfig.getRosMasterIp(), argument.host.hostname)) { + argument.isRosMaster = true; + } + + val index = rosHostArguments.indexOf(argument) + CallerSshConnectionListenerManager.invokeConnecting( + argument.host, index, isInsert + ) //正在连接 在此的 -1和true不使用 + } + } + Log.i(TAG, "${host.toString()} 连接中") + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onSshConnected(ssh: SSH) { + synchronized(sshMap) { + sshMap.put(ssh.host, ssh) + } + GlobalScope.launch(Dispatchers.Main) { + //TODO +// updateDiagnoseUIState( +// DiagnoseSource.SSH, +// "连接${Utils.getIPLastSegment(ssh.host.hostname)}成功……", +// DiagnoseType.SUCCEED +// ) + CallerSshConnectionListenerManager.invokeConnected(ssh) + } + getRosMasterConfig(ssh) + startRosHostArgumentTimer();//启动定时查询所有主机系统参数 + if (ssh.host.isHaveHadMapVersion) { + queryHadMapVersionBackup(ssh);//查询地图版本 + } + Log.i(TAG, "${ssh.host.toString()} 连接成功") + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onSshDisconnected(host: SSHHostBean) { + synchronized(sshMap) { + if (sshMap.containsKey(host)) + sshMap.remove(host) + } + GlobalScope.launch(Dispatchers.Main) { + CallerSshConnectionListenerManager.invokeDisconnected(host) + } + + Log.i(TAG, "${host.toString()} 断开连接") + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onSshConnectFailure(host: SSHHostBean, msg: String) { + Thread.sleep(20) + synchronized(sshMap) { + if (sshMap.containsKey(host)) + sshMap.remove(host) + } + val tem: Pair = getRosHostArgument(host) + if (tem.first > -1) { + tem.second!!.isConnectFailure = true + tem.second!!.connectFailureReason = msg + tem.second!!.initData() + tem.second!!.host.isHaveHadMapVersion = false + } + if (TextUtils.equals(SSHAccountConfig.getRosMasterIp(), host.hostname)) { + defaultSSH = null + } + GlobalScope.launch(Dispatchers.Main) { + //TODO +// updateDiagnoseUIState( +// DiagnoseSource.SSH, +// "连接${Utils.getIPLastSegment(host.hostname)}失败,原因:$msg……", +// DiagnoseType.FAILED +// ) + CallerSshConnectionListenerManager.invokeConnectFailure(host, msg) + } + updateDiagnoseFinish(host) + Log.i(TAG, "${host.toString()} 连接失败=${msg}") + } + + /*********************SSH连接状态***************************/ + private fun updateDockerInfoMap( + host: SSHHostBean, + list: ArrayList, + isNotify: Boolean + ) { + if (list.isNotEmpty()) { + dockerInfoMap[host] = list + //TODO +// updateDiagnoseUIStateInUIThread( +// DiagnoseSource.SSH, +// "${Utils.getIPLastSegment(host.hostname)} Docker信息获取成功……" +// ) + val mapDocker: DockerInfo? = + list.find { it.names == "autocar_default_1" || it.names == "autocar-default-1" } + if (mapDocker != null) { + val tem: Pair = getRosHostArgument(host) + if (tem.first > -1 && !mapDocker.image.isNullOrEmpty() && mapDocker.image.contains( + ":" + ) + ) { + val versionArray = mapDocker.image.split(":") + val v = versionArray[1] + tem.second!!.dockerVersion = v + if (host === defaultSSH?.host) { + FlowBus.with(EventKey.SEND_ROS_MASTER_MAP_VERSION) + .post(scopeSubscriber, v) + } + } + } + updateDiagnoseFinish(host) + } else { + if (dockerInfoMap.containsKey(host)) + dockerInfoMap.remove(host) + } + if (isNotify) FlowBus.with(EventKey.QUERY_DOCKER_PS).post( + scopeSubscriber, + DockerBean(host, list) + ) + } + + //根据index 是否等于-1判断rosHostArguments中是否存在此查找数据 + @Synchronized + private fun getRosHostArgument(key: SSHHostBean): Pair { + val tem = RosHostArgument(key) + var index = rosHostArguments.indexOf(tem) + val rosHostConfig: RosHostArgument + if (index > -1) { + rosHostConfig = rosHostArguments[index] + } else { + index = -1 + rosHostConfig = tem + } + return Pair(index, rosHostConfig) + } + + @Synchronized + private fun startRosHostArgumentTimer() { + if (rosHostArgumentTimer.get() == null) { + val timer = Timer() + rosHostArgumentTimer.set(timer) + timer.schedule(object : TimerTask() { + override fun run() { + queryRosHostArgumentMap.clear() + for (ssh in sshMap.values) { + if (ssh.isConnected) { + synchronized(queryRosHostArgumentMap) { + queryRosHostArgumentMap[ssh.host] = false + } + queryRosHostArgument(ssh) + } + } + } + }, 8000L, 5000L) //延时 + } + } + + @Synchronized + fun stopRosHostArgumentTimer() { + if (rosHostArgumentTimer.get() != null) { + rosHostArgumentTimer.get()?.cancel() + rosHostArgumentTimer.set(null) + } + } + + + //SSH命令执行结果 + override fun onExecResult( + host: SSHHostBean, + cmd: String, + isNotify: Boolean, + result: String? + ) { + if (MogoCommand.QUERY_ROS_SLAVE == cmd) { + if (defaultSSH != null && host == defaultSSH?.host) { + val remoteHost = HashMap() + try { + val lines = result!!.split("\n".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val hosts: MutableList = ArrayList() + for (line in lines) { + if (!line.startsWith("#") && (line.contains("rosmaster") || line.contains( + "rosslave" + )) + ) { + val parts = line.trim { it <= ' ' }.split("\\s+".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + if (parts.size >= 2) { + val ip = parts[0] + for (i in 1 until parts.size) { + val host = parts[i].trim { it <= ' ' } + remoteHost[host] = ip.trim { it <= ' ' } + if (host.contains("rosslave") && defaultSSH != null && ip != defaultSSH!!.host.hostname) { + val userName: String = defaultSSH!!.host.username + val hostPwd: String = defaultSSH!!.host.userPwd + hosts.add( + SSH.createHost( + userName, + hostPwd, + ip.trim { it <= ' ' }) + ) + } + } + } + } + } +// hosts.add( +// SSH.createHost( +// "mogo", +// "1", +// "192.168.1.107" +// ) +// ) + var rosHostCount = 2 + if (hosts.isNotEmpty()) { + rosHostCount = 1 + hosts.size + hosts.sortWith { host1, host2 -> + host1.hostname.compareTo(host2.hostname) + } + val host107 = hosts.find { it.hostname.endsWith("107") } + host107?.run { + isHaveHadMapVersion = true + setHdMapVer(this, null) + } ?: run { + val host103 = hosts.find { it.hostname.endsWith("103") } + host103?.run { + isHaveHadMapVersion = true + setHdMapVer(this, null) + } + } + for (h in hosts) { + openConnection(h) + } + } else { + rosHostCount = 1 + } + FlowBus.with(EventKey.SEND_ROS_HOST_COUNT) + .post(scopeSubscriber, rosHostCount) + } catch (e: Exception) { + e.printStackTrace() + } + + } + } else if (MogoCommand.QUERY_VEHICLE_CONFIG == cmd) { + vehicleConfig = VehicleConfig(result) + //TODO +// updateDiagnoseUIStateInUIThread( +// DiagnoseSource.SSH, +// "车辆信息,车牌:${vehicleConfig!!.plate} 品牌:${vehicleConfig!!.brand} 类型:${vehicleConfig!!.model}", +// DiagnoseType.SUCCEED +// ) + if (!vehicleConfig!!.plate.isNullOrEmpty()) { + getCarInfoByParam(vehicleConfig!!.plate) + findDrivers() + } + + } else if (MogoCommand.QUERY_DOCKER_PS_A == cmd) { + if (result.isNullOrEmpty()) { + // -a命令获取失败 是用 docker ps命令重新查询 + sshMap[host]?.execCommand(MogoCommand.QUERY_DOCKER_PS, isNotify); + } else { + val list = ArrayList() + try { + val lines = + result.trim { it <= ' ' }.split("\n".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + for (line in lines) { + val dockerInfo = + GsonUtils.fromJson(line, DockerInfo::class.java) + list.add(dockerInfo) + } + } catch (e: Exception) { + e.printStackTrace() + } + updateDockerInfoMap(host, list, isNotify) + } + } else if (MogoCommand.QUERY_DOCKER_PS == cmd) { + val list = ArrayList() + try { + val lines = + result!!.trim { it <= ' ' }.split("\n".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + for (line in lines) { + if (!line.startsWith("CONTAINER")) { + val parts = line.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val image = parts[1].trim { it <= ' ' } + val name = parts[parts.size - 1].trim { it <= ' ' } + var status = "" + val stringBuilder = StringBuilder() + for (i in parts.indices) { + val part = parts[i].lowercase(Locale.getDefault()) + if ("created" == part || "exited" == part || "paused" == part || "restarting" == part || "dead" == part || "removing" == part || "up" == part || "running" == part) { + for (j in i until parts.size - 1) { + stringBuilder.append(parts[j]).append(" ") + } + break + } + } + status = stringBuilder.toString().trim { it <= ' ' } + val dockerInfo = DockerInfo() + dockerInfo.image = image + dockerInfo.names = name + if (status.contains("0.")) { + status = status.substring(0, status.indexOf("0.")) + } + dockerInfo.status = status + list.add(dockerInfo) + } + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + updateDockerInfoMap(host, list, isNotify) + } else if (MogoCommand.QUERY_DF_H == cmd) { + FlowBus.with(EventKey.QUERY_DISK_STATUS).post( + scopeSubscriber, DiskInfo(host, result) + ) + } else if (MogoCommand.QUERY_STARTUP_CONFIG == cmd) { + var configs: MutableList? = null + if (!result.isNullOrEmpty()) { + val gson = Gson() + configs = mutableListOf() + val type = object : TypeToken>() {}.type + val data: Map = gson.fromJson(result, type) + for ((key, value) in data) { + if (vehicleConfig != null && !vehicleConfig!!.plate.isNullOrEmpty() && vehicleConfig!!.plate != "未连接") { + if (key.contains(vehicleConfig!!.plate)) { + configs.add(StartupConfig.Config(key, value)) + } + } + } + } + FlowBus.with(EventKey.QUERY_STARTUP_CONFIG).post( + scopeSubscriber, + StartupConfig(host, configs) + ) + } else { + val pair: Pair = getRosHostArgument(host) + if (pair.first > -1) { + val config: RosHostArgument = pair.second!! + when (cmd) { + MogoCommand.QUERY_MEM_TOTAL -> { + //查询总内存 + config.setMemTotal(result) + } + + MogoCommand.QUERY_MEM_USED -> { + //查询已用内存 + config.setMemUsed(result) + } + + MogoCommand.QUERY_SWAP_TOTAL -> + //查询总的交换分区容量 + config.setSwapTotal(result) + + MogoCommand.QUERY_SWAP_USED -> + //查询用户使用的交换分区容量 + config.setSwapUsed(result) + + MogoCommand.QUERY_CPU_USAGE_RATE -> { + //查询CPU使用率 + config.setCpuUsageRate(result) + } + + MogoCommand.QUERY_DISK_DATA -> { + //查询磁盘总大小 + config.setDiskData(result) + } + + MogoCommand.QUERY_DISK_DATA_USED -> { + //查询磁盘使用大小 + config.setDiskDataUsed(result) + } + + MogoCommand.QUERY_RUN_TIME -> { + //查询运行小时数 + config.setRunTime(result) + } + + MogoCommand.QUERY_LOGON_COUNT -> { + //获取登录用户数量 + } + + MogoCommand.QUERY_CPU_CORE -> { + //CPU内核数量 + } + + MogoCommand.QUERY_LOAD_1_5_15 -> { + //查询负载 + config.load = result + //更新Ros Host 参数列表 + FlowBus.with(EventKey.QUERY_ROS_HOST_STATUS) + .post( + scopeSubscriber, pair.first + ) + updateSystemResourceRedDot(host) + } + } + } + } +// Log.i(TAG,"${host.toString()} 执行命令=${cmd} 结果=${result} 是否需要通知=${isNotify}") + } + + //更新梓潼资源页面异常数据数量 + private fun updateSystemResourceRedDot(host: SSHHostBean) { + if (queryRosHostArgumentMap.isNotEmpty()) { + if (queryRosHostArgumentMap.containsKey(host)) + queryRosHostArgumentMap[host] = true + if (queryRosHostArgumentMap.values.all { it }) { + FlowBus.with(EventKey.UPDATE_SYSTEM_RESOURCE_RED_DOT) + .post( + scopeSubscriber, + "" + ) + } + } + } + + //Docker连接状态 + override fun onDockerStatus(host: SSHHostBean, status: Int, isNotify: Boolean) { + if (isNotify) + FlowBus.with(EventKey.DOCKER_STATUS) + .post(scopeSubscriber, DockerStatus(host, status)) + } + + //Docker命令执行结果 + override fun onDockerExecResult( + host: SSHHostBean, + cmd: String, + result: String?, + isNotify: Boolean + ) { + if (cmd.startsWith(MogoCommand.QUERY_DOCKER_CONFIG_CONTENT) && isNotify) { + FlowBus.with(EventKey.QUERY_DOCKER_CONFIG_CONTENT).post( + scopeSubscriber, + DockerConfigContent(host, cmd, result) + ) + } else if (cmd == MogoCommand.QUERY_HADMAP_ENGINE_VERSION_BACKUP) { + val ssh = getConnectedSSH(host) + if (ssh != null) { + if (TextUtils.isEmpty(result)) { + queryHadMapVersion(ssh) + } else { + ssh.systemDisconnectDocker() + setHdMapVer(host, result) + } + } + } else if (cmd == MogoCommand.QUERY_HADMAP_ENGINE_VERSION) { + val ssh = getConnectedSSH(host) + ssh?.systemDisconnectDocker() + val msg = if (result.isNullOrEmpty()) { + getString(R.string.rviz_fmd_get_fail_hd_map_version) + } else { + result + } + setHdMapVer(host, msg) + } else if (cmd == String.format( + MogoCommand.FIND_DRIVER_CAMERA, + vehicleConfig!!.plate + ) + ) { + val sensorStatus = ArrayList() + try { + + val lines = + result!!.trim { it <= ' ' }.split("\n".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + for (line in lines) { + + for (sensor in SensorCamera.values()) { + if (sensor.fileName == line) { + sensorStatus.add(SensorStatusEntity(sensor.title, true)) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + sensorStatus.clear() + } + if (sensorStatus.isEmpty()) { + sensorStatus.add( + SensorStatusEntity( + SensorCamera.DriversCameraSensing30.title, + true + ) + ) + sensorStatus.add( + SensorStatusEntity( + SensorCamera.DriversCameraSensing60.title, + true + ) + ) + sensorStatus.add( + SensorStatusEntity( + SensorCamera.DriversCameraSensing120.title, + true + ) + ) + sensorStatus.add( + SensorStatusEntity( + SensorCamera.DriversCameraSensing120Left.title, + true + ) + ) + sensorStatus.add( + SensorStatusEntity( + SensorCamera.DriversCameraSensing120Back.title, + true + ) + ) + sensorStatus.add( + SensorStatusEntity( + SensorCamera.DriversCameraSensing120Right.title, + true + ) + ) + } + FlowBus.with>(EventKey.INIT_SENSOR_CAMERA).post( + scopeSubscriber, + ArrayList(sensorStatus.sortedBy { it.sensorName }) + ) + + Log.i(TAG, "查询摄像头信息=${result}") + } /*else if (cmd == String.format(MogoCommand.FIND_DRIVER_LIDAR, vehicleConfig!!.plate)) { + Log.i(TAG,"激光雷达信息=${result}") + } else if (cmd == String.format(MogoCommand.FIND_DRIVER_RADAR, vehicleConfig!!.plate)) { + Log.i(TAG,"毫米波雷达信息=${result}") + }*/ + } + + private fun setHdMapVer(host: SSHHostBean, ver: String?) { + Log.i(TAG, "高精地图版本=${host.hostname} ver:$ver ") + hdMapVersion.ip = host.hostname + if (!ver.isNullOrEmpty()) + hdMapVersion.version = ver + FlowBus.with(EventKey.SEND_HD_MAP_VERSION).post( + scopeSubscriber, + hdMapVersion + ) + } + + /****************************************自动查询命令 */ //获取ROS MASTER 相关配置 + @OptIn(DelicateCoroutinesApi::class) + private fun getRosMasterConfig(ssh: SSH) { + if (TextUtils.equals(SSHAccountConfig.getRosMasterIp(), ssh.host?.hostname)) { + defaultSSH = ssh + rosHostArguments.isNotEmpty().let { + val iterator = rosHostArguments.iterator() + while (iterator.hasNext()) { + val data = iterator.next() + if (data.isConnectFailure) { + val position = rosHostArguments.indexOf(data) // 获取删除的位置 + iterator.remove() // 从数据列表中删除元素 + FlowBus.with(EventKey.REMOVE_ROS_HOST_ITEM) + .post( + scopeSubscriber, position + ) + } + } + } + ssh.execCommand(MogoCommand.QUERY_VEHICLE_CONFIG, false) //获取车牌等信息 + ssh.execCommand(MogoCommand.QUERY_ROS_SLAVE, false) //获取从ros主机 + } + //TODO +// updateDiagnoseUIStateInUIThread( +// DiagnoseSource.SSH, +// "正在获取${Utils.getIPLastSegment(ssh.host.hostname)} Docker信息……" +// ) + ssh.execCommand(MogoCommand.QUERY_DOCKER_PS_A, false) + //等待获取Docker信息是否成功 + GlobalScope.launch(Dispatchers.IO) { + delay(6000) + if (!dockerInfoMap.containsKey(ssh.host)) { + //TODO +// withContext(Dispatchers.Main) { +// updateDiagnoseUIState( +// DiagnoseSource.SSH, +// "${Utils.getIPLastSegment(ssh.host.hostname)} Docker信息获取失败……", +// DiagnoseType.FAILED +// ) +// } + updateDiagnoseFinish(ssh.host) + } + } + } + + //检查诊断是否完成,并更新状态 + private fun updateDiagnoseFinish(host: SSHHostBean) { + //TODO +// if (!isDiagnoseFinish.get()) { +// synchronized(diagnoseFinishState) { +// if (diagnoseFinishState.containsKey(host)) { +// diagnoseFinishState[host] = true +// } +// } +// val count = diagnoseFinishState.count { it.value } +// if (count == diagnoseFinishState.size) { +// isDiagnoseFinish.set(true) +// val msg = +// if (CallerAutopilotActionsListenerManager.isAutopilotAbility()) "可自动驾驶,请打开「鹰眼」进入运营状态" else "不可自动驾驶,详细信息请关闭《诊断弹窗》查看" +// updateDiagnoseUIStateInUIThread(DiagnoseSource.MC, "诊断完成,当前环境${msg}") +// val vib = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +// val vibratorManager = +// getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager +// vibratorManager.defaultVibrator +// } else { +// getSystemService(VIBRATOR_SERVICE) as Vibrator +// } +// vib.vibrate(VibrationEffect.createOneShot(500, 255)) +// } +// } + } + + private fun queryRosHostArgument(ssh: SSH) { + ssh.execCommand(MogoCommand.QUERY_MEM_TOTAL, false);//查询总内存 + ssh.execCommand(MogoCommand.QUERY_MEM_USED, false);//查询已用内存 + ssh.execCommand(MogoCommand.QUERY_SWAP_TOTAL, false);//查询总的交换分区容量 + ssh.execCommand(MogoCommand.QUERY_SWAP_USED, false);//查询用户使用的交换分区容量 + ssh.execCommand(MogoCommand.QUERY_CPU_USAGE_RATE, false);//查询CPU使用率 + ssh.execCommand(MogoCommand.QUERY_DISK_DATA, false);//查询磁盘总大小,主要查/data目录 + ssh.execCommand(MogoCommand.QUERY_DISK_DATA_USED, false);//查询磁盘使用大小,主要查/data目录 + ssh.execCommand(MogoCommand.QUERY_RUN_TIME, false);//查询运行时间 + ssh.execCommand(MogoCommand.QUERY_LOGON_COUNT, false);//获取登录用户数量 + ssh.execCommand(MogoCommand.QUERY_CPU_CORE, false);//CPU内核数量 + ssh.execCommand(MogoCommand.QUERY_LOAD_1_5_15, false);//查询负载 + } + + + private fun queryHadMapVersionBackup(ssh: SSH) { + setHdMapVer(ssh.host, "获取中") + if (!ssh.isDockerOpened) { + ssh.systemConnectDocker() + } + ssh.systemExecDockerCommand(MogoCommand.QUERY_HADMAP_ENGINE_VERSION_BACKUP) + } + + private fun queryHadMapVersion(ssh: SSH) { + setHdMapVer(ssh.host, "获取中") + if (!ssh.isDockerOpened) { + ssh.systemConnectDocker() + } + ssh.systemExecDockerCommand(MogoCommand.QUERY_HADMAP_ENGINE_VERSION) + } + + //查找驱动 + private fun findDrivers() { + if (defaultSSH != null && vehicleConfig != null && !vehicleConfig!!.plate.isNullOrEmpty()) { + if (!defaultSSH!!.isDockerOpened) { + defaultSSH!!.systemConnectDocker() + } + defaultSSH!!.systemExecDockerCommand( + String.format( + MogoCommand.FIND_DRIVER_CAMERA, + vehicleConfig!!.plate + ) + ) +// defaultSSH!!.systemExecDockerCommand( +// String.format( +// MogoCommand.FIND_DRIVER_LIDAR, +// vehicleConfig!!.plate +// ) +// ) +// defaultSSH!!.systemExecDockerCommand( +// String.format( +// MogoCommand.FIND_DRIVER_RADAR, +// vehicleConfig!!.plate +// ) +// ) + } + + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FmCodeUpdateService.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FmCodeUpdateService.kt new file mode 100644 index 0000000000..8467cc4ad0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/service/FmCodeUpdateService.kt @@ -0,0 +1,104 @@ +package com.zhjt.mogo_core_function_devatools.rviz.service + +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import com.mogo.eagle.core.utilcode.util.FileIOUtils +import com.mogo.eagle.core.utilcode.util.PathUtils +import com.mogo.eagle.core.utilcode.util.ServiceUtils +import com.mogo.eagle.core.utilcode.util.Utils +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.LambdaTask + + +/** + * 故障码更新服务 + * 这里的逻辑是从服务器获取最新的故障码db文件,判断版本是否需要更新本地文件,如果需要更新则从网络下载文件存储到 + * /data/data/com.mogo.rviz/databases/ 替换已有的 AutoPilotVisualDB.db、AutoPilotVisualDB.db-shm、AutoPilotVisualDB.db-wal + * + * + * /data/data/com.mogo.rviz/databases/AutoPilotVisualDB.db + * /data/data/com.mogo.rviz/databases/AutoPilotVisualDB.db-shm + * /data/data/com.mogo.rviz/databases/AutoPilotVisualDB.db-wal + */ +class FmCodeUpdateService : Service() { + private val TAG = "FmCodeUpdateService" + override fun onCreate() { + super.onCreate() + // 在这里调用startForeground()方法 +// startForeground(NOTIFICATION_ID, notification); + Log.d(TAG, "启动 故障码数据库更新服务……") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + copyDbFileToDatabases() + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + // 在这里调用stopForeground()方法 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_DETACH); // 或 STOP_FOREGROUND_REMOVE + } else { + stopForeground(true); // true = remove notification + } + Log.d(TAG, "关闭 故障码数据库更新服务……") + } + + // 1、 请求网络接口获取最新的 db 文件版本及下载地址 + private fun checkVersion() {} + + // 2、 将接口返回的数据库版本对比本地数据库版本对比,如果大于本地版本则进行文件下载 + private fun downloadDbFile() {} + + // 3、 下载进度对外展示Loading,下载成功、失败结果展示, + private fun onDownLoadFinish() {} + private fun onDownLoadFault() {} + private fun onDownLoadProgress(progress: Int) {} + + // 如果下载失败不可阻塞主要流程 + // 4、 下载成功后直接替 /data/data/com.mogo.rviz/databases/ 路径下的文件,每次替换 3 个文件,缺少一个都会导致数据丢失 + private fun copyDbFileToDatabases() { + LambdaTask { + Log.d(TAG, "开始替换 /data/data/com.mogo.rviz/databases/ 下的数据库……") + val assetManager = Utils.getApp().assets + + val pathAutoPilotVisualDB = + PathUtils.getInternalAppDbsPath() + "/AutoPilotVisualDB.db" + val pathAutoPilotVisualDBShm = + PathUtils.getInternalAppDbsPath() + "/AutoPilotVisualDB.db-shm" + val pathAutoPilotVisualDBWal = + PathUtils.getInternalAppDbsPath() + "/AutoPilotVisualDB.db-wal" + + // 这里暂时不做是否存在判断,直接替换 + var inputStream = assetManager.open("AutoPilotVisualDB.db"); + FileIOUtils.writeFileFromIS(pathAutoPilotVisualDB, inputStream) + Log.d(TAG, "替换 $pathAutoPilotVisualDB 成功") + inputStream = assetManager.open("AutoPilotVisualDB.db-shm"); + FileIOUtils.writeFileFromIS(pathAutoPilotVisualDBShm, inputStream) + Log.d(TAG, "替换 $pathAutoPilotVisualDBShm 成功") + inputStream = assetManager.open("AutoPilotVisualDB.db-wal"); + FileIOUtils.writeFileFromIS(pathAutoPilotVisualDBWal, inputStream) + Log.d(TAG, "替换 $pathAutoPilotVisualDBWal 成功") + + Log.d(TAG, "完成数据库替换操作……") + +// 这里是生成 DB 文件,如果 「故障分析列表」更新,需要对DB文件进行更新,并将更新后的数据库给到 后台 青龙 进行升级操作 +// https://doc.weixin.qq.com/sheet/e3_AT0ANQaoAPsbcirKpQFSxuL1csc3B?scode=AEwAGwfJAA4Pa1cAYOAIsARAY7ALY +// FmCodeEntity.csvToBeanByNameAnnotation(context)已经删除 改用tool_fm_file_to_db 工具进行数据库更新 +// FmCodeEntity.getAllList(context).forEach { +// println("查询到错误信息:${it.faultCode}") +// } + + ServiceUtils.stopService(this.javaClass.name) + }.execute() + + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/SSH.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/SSH.java new file mode 100644 index 0000000000..8da21cfb70 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/SSH.java @@ -0,0 +1,399 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * 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. + */ + +package com.zhjt.mogo_core_function_devatools.rviz.ssh; + +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.crypto.keys.Ed25519Provider; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnDockerExecCommandListener; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnExecCommandListener; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnSshConnectionListener; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.DockerCommandHandler; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.ExecCommandHandler; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +import java.io.IOException; +import java.net.NoRouteToHostException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLException; + + +/** + * @author Kenny Root + */ +public class SSH implements ConnectionMonitor, OnExecCommandListener, OnDockerExecCommandListener { + static { + // Since this class deals with Ed25519 keys, we need to make sure this is available. + Ed25519Provider.insertIfNeeded(); + } + + public final SSHHostBean host; + + + public SSH(SSHHostBean host) { + this.host = host; + } + + + private static final String PROTOCOL = "ssh"; + private static final String TAG = SSH.class.getSimpleName(); + private static final int DEFAULT_PORT = 22; + + + private static final Pattern hostmask = Pattern.compile( + "^(.+)@(([0-9a-z.-]+)|(\\[[a-f:0-9]+\\]))(:(\\d+))?$", Pattern.CASE_INSENSITIVE); + + private volatile boolean authenticated = false; + private volatile boolean connected = false; + private Connection connection; + + private OnExecCommandListener onExecCommandListener; + private OnDockerExecCommandListener onDockerExecCommandListener; + private OnSshConnectionListener onSshConnectionListener; + private ExecCommandHandler execCommandHandler; + private DockerCommandHandler dockerCommandHandler; + + + /** + * Internal method to request actual PTY terminal once we've finished + * authentication. If called before authenticated, it will just fail. + */ + private void finishConnection() throws IOException { + authenticated = true; + if (!host.getWantSession()) { + Log.e("SSH", "由于主机设置不能起动该会话"); + if (onSshConnectionListener != null) { + onSshConnectionListener.onSshConnectFailure(host, "由于主机设置不能起动该会话"); + } + return; + } + if (execCommandHandler == null) { + execCommandHandler = new ExecCommandHandler(host, connection, this); + execCommandHandler.start(); + } + if (dockerCommandHandler == null) { + dockerCommandHandler = new DockerCommandHandler(host, connection, this); + } + if (onSshConnectionListener != null) { + onSshConnectionListener.onSshConnected(this); + } + } + + @Override + public void onExecResult(@NonNull SSHHostBean host, @NonNull String cmd, boolean isNotify, String result) { + if (onExecCommandListener != null) { + onExecCommandListener.onExecResult(host, cmd, isNotify, result); + } + } + + + @Override + public void onDockerStatus(@NonNull SSHHostBean host, int status, boolean isNotify) { + if (onDockerExecCommandListener != null) { + onDockerExecCommandListener.onDockerStatus(host, status, isNotify); + } + } + + + @Override + public void onDockerExecResult(@NonNull SSHHostBean host, @NonNull String cmd, String result, boolean isNotify) { + if (onDockerExecCommandListener != null) { + onDockerExecCommandListener.onDockerExecResult(host, cmd, result, isNotify); + } + } + + public void connect() { + Thread connectionThread = new Thread(new Runnable() { + @Override + public void run() { + if (onSshConnectionListener != null) { + onSshConnectionListener.onSshConnecting(host, -1, true);//正在连接 在此的 -1和true不使用 + } + connection = new Connection(host.getHostname(), host.getPort()); + connection.addConnectionMonitor(SSH.this); + try { + connection.connect(); + connected = true; + } catch (IOException e) { + Log.e(TAG, "Problem in SSH connection thread during authentication", e); + callConnectFailure(e.getCause()); + return; + } + authenticate(); + } + }); + connectionThread.setName("Connection"); + connectionThread.setDaemon(true); + connectionThread.start(); + } + + private void authenticate() { + try { + //尝试在 SSH 连接中使用“无身份验证”方式进行连接,也就是不进行密码或密钥的身份验证。 + if (connection.authenticateWithNone(host.getUsername())) { + finishConnection(); + return; + } + if (connection.authenticateWithPassword(host.getUsername(), host.getUserPwd())) { + finishConnection(); + } else { + callConnectFailure("用户名或密码错误,请重试"); + } + + } catch (IllegalStateException e) { + Log.e(TAG, "Connection went away while we were trying to authenticate", e); + callConnectFailure("连接被中断,请检查网络"); + } catch (Exception e) { + Log.e(TAG, "Problem during handleAuthentication()", e); + callConnectFailure(e.getCause()); + } + } + + private void callConnectFailure(String msg) { + if (onSshConnectionListener != null) { + onSshConnectionListener.onSshConnectFailure(host, msg); + } + close(true); + } + + private void callConnectFailure(Throwable rootCause) { + String msg; + if (rootCause instanceof SocketTimeoutException) { + msg = "连接超时,请检查网络"; + } else if (rootCause instanceof UnknownHostException) { + msg = "无法解析此IP,请检查输入是否有误"; + } else if (rootCause instanceof NoRouteToHostException) { + msg = "无法找到目标主机,请检查网络或主机地址"; + } else if (rootCause instanceof SSLException) { + msg = "SSL异常,请重试"; + } else if (rootCause instanceof SocketException) {//ConnectException和SocketException均为网络异常 例如没有网络,强行断网 + msg = "网络连接异常,请检查网络或主机"; + } else if (rootCause instanceof IOException) { + msg = "连接丢失或通道异常,请检查主机"; + } else { + msg = "连接失败,请重试"; + } + callConnectFailure(msg); + } + + + public void close() { + Thread disconnectThread = new Thread(new Runnable() { + @Override + public void run() { + if (isConnected()) + close(false); + } + }); + disconnectThread.setName("Disconnect"); + disconnectThread.start(); + } + + private void close(boolean isFailure) { + connected = false; + if (execCommandHandler != null) { + execCommandHandler.stop(); + execCommandHandler = null; + } + disconnectDocker(false); + if (connection != null) { + connection.close(); + connection = null; + } + if (!isFailure) { + if (onSshConnectionListener != null) { + onSshConnectionListener.onSshDisconnected(host); + } + } + } + + + public static String getProtocolName() { + return PROTOCOL; + } + + + public boolean isConnected() { + return connected; + } + + @Override + public void connectionLost(Throwable reason) { + Log.i(TAG, host.getHostname() + " 连接丢失 " + reason); + //主动关闭Closed due to user request. 其他为异常关闭 + //There was a problem during connect. 没有给出具体原因,可以根据其他方式获取到所以屏蔽掉 + if (!"Closed due to user request.".equals(reason.getMessage()) && !"There was a problem during connect.".equals(reason.getMessage())) { + callConnectFailure(reason); + } + } + + + public int getDefaultPort() { + return DEFAULT_PORT; + } + + public static String getDefaultNickname(String username, String hostname, int port) { + if (port == DEFAULT_PORT) { + return String.format(Locale.US, "%s@%s", username, hostname); + } else { + return String.format(Locale.US, "%s@%s:%d", username, hostname, port); + } + } + + public static Uri getUri(String input) { + Matcher matcher = hostmask.matcher(input); + if (!matcher.matches()) + return null; + StringBuilder sb = new StringBuilder(); + sb.append(PROTOCOL) + .append("://") + .append(Uri.encode(matcher.group(1))) + .append('@') + .append(Uri.encode(matcher.group(2))); + String portString = matcher.group(6); + int port = DEFAULT_PORT; + if (portString != null) { + try { + port = Integer.parseInt(portString); + if (port < 1 || port > 65535) { + port = DEFAULT_PORT; + } + } catch (NumberFormatException nfe) { + // Keep the default port + } + } + if (port != DEFAULT_PORT) { + sb.append(':').append(port); + } + sb.append("/#").append(Uri.encode(input)); + return Uri.parse(sb.toString()); + } + + + public static SSHHostBean createHost(String username, String userPwd, String hostname) { + SSHHostBean host = new SSHHostBean(); + host.setHostname(hostname); + host.setPort(DEFAULT_PORT); + host.setUsername(username); + host.setUserPwd(userPwd); + host.setNickname(getDefaultNickname(host.getUsername(), host.getHostname(), host.getPort())); + return host; + } + + public static SSHHostBean createHost(Uri uri) { + SSHHostBean host = new SSHHostBean(); + host.setHostname(uri.getHost()); + int port = uri.getPort(); + if (port < 0) + port = DEFAULT_PORT; + host.setPort(port); + host.setUsername(uri.getUserInfo()); + String nickname = uri.getFragment(); + if (nickname == null || nickname.length() == 0) { + host.setNickname(getDefaultNickname(host.getUsername(), + host.getHostname(), host.getPort())); + } else { + host.setNickname(uri.getFragment()); + } + return host; + } + + + public void setOnSshConnectionListener(OnSshConnectionListener onSshConnectionListener) { + this.onSshConnectionListener = onSshConnectionListener; + } + + public void setExecCommandListener(OnExecCommandListener listener) { + onExecCommandListener = listener; + } + + public void setExecDockerCommandListener(OnDockerExecCommandListener listener) { + onDockerExecCommandListener = listener; + } + + public void execCommand(String cmd, boolean isNotify) { + if (execCommandHandler != null) + execCommandHandler.execCommand(cmd, isNotify); + } + + + public void startDockerDisconnectTimer(boolean isNotify) { + if (dockerCommandHandler != null) { + dockerCommandHandler.startDisconnectTimer(isNotify); + } + } + + public void stopDockerDisconnectTimer() { + if (dockerCommandHandler != null) { + dockerCommandHandler.stopDisconnectTimer(); + } + } + + public void systemConnectDocker() { + if (dockerCommandHandler != null) { + dockerCommandHandler.systemConnectDocker(); + } + } + + public void connectDocker() { + if (dockerCommandHandler != null) { + dockerCommandHandler.userConnectDocker(); + } + } + + public void systemDisconnectDocker() { + if (dockerCommandHandler != null) { + dockerCommandHandler.systemDisconnectDocker(); + } + } + + public void disconnectDocker(boolean isNotify) { + if (dockerCommandHandler != null) { + dockerCommandHandler.disconnectDocker(isNotify); + } + } + + public boolean isDockerOpened() { + return dockerCommandHandler != null && dockerCommandHandler.isDockerOpened(); + } + + public void systemExecDockerCommand(String cmd) { + if (dockerCommandHandler != null) { + dockerCommandHandler.systemExecDockerCommand(cmd); + } + } + + public void execDockerCommand(String cmd, boolean isNotify) { + if (dockerCommandHandler != null) { + dockerCommandHandler.execDockerCommand(cmd, isNotify); + } + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/MogoCommand.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/MogoCommand.java new file mode 100644 index 0000000000..09a27aeed8 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/MogoCommand.java @@ -0,0 +1,50 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.constant; + +/** + * MOGO 相关命令 + */ +public final class MogoCommand { + public static final String QUERY_ROS_SLAVE = "cat /etc/hosts";//查询主从ROS主机 + public static final String QUERY_VEHICLE_CONFIG = "cat /data/autocar/vehicle_monitor/vehicle_config.txt";//查询车配车辆信息 + public static final String QUERY_CPU_CORE = "awk '/processor/{core++} END{print core}' /proc/cpuinfo";//CPU内核数量 + public static final String QUERY_CPU_USAGE_RATE = "top -b -n 1 | grep \"Cpu(s)\" | awk '{print $2 + $4}' | tr -d '\n'";//CPU使用率 + + + public static final String QUERY_LOGON_COUNT = "who | wc -l | tr -d '\n'";//获取登录用户数量 + + public static final String QUERY_MEM_TOTAL = "free -m | awk '/Mem/{printf \"%.2fG\", $2/1024}'";//总内存容量 + public static final String QUERY_MEM_USED = "free -m | awk 'NR==2{printf \"%.1fG\",($2-$NF)/1024}'";//用户已用内存容量 + public static final String QUERY_MEM_AVAILABLE = "free -m | awk 'NR==2{printf \"%.2fG\",$NF/1024}'";//剩余可用内存容量 + public static final String QUERY_MEM_PERCENTAGE = "free -m | awk '/Mem/{printf \"%.0f%\", ($2-$NF)/$2*100}'";//内存使用占比 + + public static final String QUERY_SWAP_TOTAL = "free -m | awk '/Swap/{printf \"%.2fG\", $2/1024}'";//总的交换分区容量 + public static final String QUERY_SWAP_USED = "free -m | awk '/Swap/{printf \"%.2fG\",$3/1024}'";//用户使用的交换分区容量 + public static final String QUERY_SWAP_FREE = "free -m | awk '/Swap/{printf \"%.2fG\",$4/1024}'";//剩余交换分区容量 + public static final String QUERY_SWAP_PERCENTAGE = "free -m | awk '/Swap/{printf \"%.2f\",$4/$2*100}'";//可用交换分区占比 + + public static final String QUERY_RUN_TIME = "top -b -n 1 | awk -F 'up | user' 'NR==1 {gsub(/, [^,]*$/, \"\", $2); print $2}'";//运行时间截取 + public static final String QUERY_LOAD_1_5_15 = "top -b -n 1 | grep -oP 'load average: \\d+\\.\\d+, \\d+\\.\\d+, \\d+\\.\\d+' | awk '{print $3, $4, $5}'";//负载截取 + + public static final String QUERY_DISK_DATA = "df -h /data/ | awk 'NR==2{print}' | awk '{printf $2}'";//磁盘总大小,主要查/data目录(单位存在KB MB GB TB PB EB 正常情况只有GB和TB) + public static final String QUERY_DISK_DATA_USED = "df -h /data/ | awk 'NR==2{print}' | awk '{printf $3}'";//磁盘使用大小,主要查/data目录(单位存在KB MB GB TB PB EB 正常情况只有GB和TB) + public static final String QUERY_DISK_DATA_AVAIL = "df -h /data/ | awk 'NR==2{print}' | awk '{printf $4}'";// 磁盘剩余大小,主要查/data目录(单位存在KB MB GB TB PB EB 正常情况只有GB和TB) + + + public static final String QUERY_DOCKER_PS_A = "sudo docker ps -a --format '{{json .}}'"; + public static final String QUERY_DOCKER_PS = "sudo docker ps"; + public static final String QUERY_STARTUP_CONFIG = "cat /data/autocar/StartupConfigCache.json";//查询配置文件列表 + public static final String QUERY_DF_H = "df -h"; + public static final String DOCKER_BASH = "sudo docker exec -it autocar_default_1 bash"; + // public static final String QUERY_DOCKER_CONFIG_CONTENT = "sudo docker exec autocar_default_1 cat ";//查询配置文件内容 + public static final String QUERY_DOCKER_CONFIG_CONTENT = "cat ";//查询配置文件内容 + //查询当前运行 Docker 高精地图版本,2x:103 6x:107,读的时候有逻辑,db.sqlite.backup 有就以这个为准,没 db.sqlite.backup 再读dab.sqlite + public static final String QUERY_HADMAP_ENGINE_VERSION_BACKUP = "readlink /home/mogo/autopilot/share/hadmap_engine/data/hadmap_data/db.sqlite.backup | awk -F/ '{print $NF}'"; + public static final String QUERY_HADMAP_ENGINE_VERSION = "readlink /home/mogo/autopilot/share/hadmap_engine/data/hadmap_data/db.sqlite | awk -F/ '{print $NF}'"; + + public static final String QUERY_TOP = "top -b -n 1 | head -n 5";//top 命令 自行一次 只返回前5行的数据 + public static final String FIND_DRIVER_CAMERA = "find /home/mogo/data/vehicle_monitor/%s/drivers/camera/ -maxdepth 1 -type f -name \"*.launch\" -exec basename {} \\;"; + public static final String FIND_DRIVER_LIDAR = "find /home/mogo/data/vehicle_monitor/%s/drivers/lidar/ -maxdepth 1 -type f -name \"*.launch\" -exec basename {} \\;"; + public static final String FIND_DRIVER_RADAR = "find /home/mogo/data/vehicle_monitor/%s/drivers/radar/ -maxdepth 1 -type f -name \"*.launch\" -exec basename {} \\;"; + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/SSHConstant.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/SSHConstant.java new file mode 100644 index 0000000000..160b64b14b --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/constant/SSHConstant.java @@ -0,0 +1,9 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.constant; + +import java.nio.charset.Charset; + +public class SSHConstant { + public final static String AUTHAGENT_NO = "no"; + public final static String DELKEY_DEL = "del"; + public final static String ENCODING_DEFAULT = Charset.defaultCharset().name(); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/call/CallerSshConnectionListenerManager.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/call/CallerSshConnectionListenerManager.kt new file mode 100644 index 0000000000..8f5d228198 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/call/CallerSshConnectionListenerManager.kt @@ -0,0 +1,62 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.function.call + +import com.mogo.eagle.core.function.call.base.CallerBase +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnSshConnectionListener +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean + + +/** + * ADAS连接 回调监听 + */ +object CallerSshConnectionListenerManager : CallerBase() { + + + @Synchronized + fun invokeConnecting(host: SSHHostBean, rosHostArgumentPosition: Int, isInserted: Boolean) { + M_LISTENERS.forEach { + val listener = it.value + try { + listener.onSshConnecting(host, rosHostArgumentPosition, isInserted) + } catch (t: Throwable) { + t.printStackTrace() + } + } + } + + @Synchronized + fun invokeConnected(ssh: SSH) { + M_LISTENERS.forEach { + val listener = it.value + try { + listener.onSshConnected(ssh) + } catch (t: Throwable) { + t.printStackTrace() + } + } + } + + @Synchronized + fun invokeDisconnected(host: SSHHostBean) { + M_LISTENERS.forEach { + val listener = it.value + try { + listener.onSshDisconnected(host) + } catch (t: Throwable) { + t.printStackTrace() + } + } + } + + @Synchronized + fun invokeConnectFailure(host: SSHHostBean, msg: String) { + M_LISTENERS.forEach { + val listener = it.value + try { + listener.onSshConnectFailure(host, msg) + } catch (t: Throwable) { + t.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnDockerExecCommandListener.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnDockerExecCommandListener.java new file mode 100644 index 0000000000..26333384b7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnDockerExecCommandListener.java @@ -0,0 +1,20 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +public interface OnDockerExecCommandListener { + /** + * Docker连接状态 + * + * @param host Docker宿主机 + * @param status 连接状态 + * @param isNotify 是否需要通知UI更新 + */ + + void onDockerStatus(@NonNull SSHHostBean host, int status, boolean isNotify); + + + void onDockerExecResult(@NonNull SSHHostBean host, @NonNull String cmd, String result, boolean isNotify); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnExecCommandListener.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnExecCommandListener.java new file mode 100644 index 0000000000..5efcd0dbb3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnExecCommandListener.java @@ -0,0 +1,9 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +public interface OnExecCommandListener { + void onExecResult(@NonNull SSHHostBean host,@NonNull String cmd, boolean isNotify, String result); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnSshConnectionListener.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnSshConnectionListener.java new file mode 100644 index 0000000000..45b2136bda --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/function/listener/OnSshConnectionListener.java @@ -0,0 +1,43 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * 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. + */ + +package com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +/** + * 连接状态回调 + */ +public interface OnSshConnectionListener { + /** + * 正在连接 + * + * @param host 主机 + * @param rosHostArgumentPosition 主机列表中的ROS HOST 参数 的下标 + * @param isInserted 主是插入还是更新 + */ + void onSshConnecting(@NonNull SSHHostBean host, int rosHostArgumentPosition, boolean isInserted); + + void onSshConnected(@NonNull SSH ssh);//已连接 + + void onSshDisconnected(@NonNull SSHHostBean host);//断开连接 + + void onSshConnectFailure(@NonNull SSHHostBean host, @NonNull String msg);//连接失败 +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/DockerCommandHandler.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/DockerCommandHandler.java new file mode 100644 index 0000000000..76b2db9af0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/DockerCommandHandler.java @@ -0,0 +1,408 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.module; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnDockerExecCommandListener; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.Session; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * 命令Docker执行线程 + */ +public class DockerCommandHandler { + private static final String TAG = DockerCommandHandler.class.getSimpleName(); + + private static final long DEFAULT_CLOSE_TIME = 5 * 60 * 1000L; + private static final int WHAT_OPEN = 0; + private static final int WHAT_CLOSE = 1; + private static final int WHAT_EXEC_COMMAND = 2; + private static final int WHAT_CLOSE_DELAYED = 3; + + private final static String changeLine = "\n"; + private final OnDockerExecCommandListener listener; + private HandlerThread mThread; + private BaseHandler mBaseHandler; + private final SSHHostBean host; + private final Connection connection; + private Session session; + private final AtomicInteger connectStatus = new AtomicInteger(CONNECT_STATUS.CLOSE); + private final AtomicBoolean isUserAction = new AtomicBoolean(false);//是否是用户操作,如果当前存在用户操作 + + public interface CONNECT_STATUS { + + /** + * 已连接 + */ + int CONNECTED = 1; + /** + * 连接失败 + */ + int FAILED = 2; + /** + * 关闭 未连接(正常关闭) + */ + int CLOSE = 3; + /** + * 关闭 未连接(延时关闭,定时关闭) + */ + int CLOSE_DELAYED = 4; + } + + public DockerCommandHandler(@NonNull SSHHostBean host, @NonNull Connection connection, @NonNull OnDockerExecCommandListener listener) { + this.host = host; + this.connection = connection; + this.listener = listener; + + } + + //系统启动Docker + public void systemConnectDocker() { + connectDocker(false); + } + + //用户启动Docker + public void userConnectDocker() { + this.isUserAction.set(true); + connectDocker(true); + } + + private synchronized void connectDocker(boolean isNotify) { + startThread(); + Message msg = Message.obtain(); + msg.what = WHAT_OPEN; + msg.arg1 = isNotify ? 1 : 0; + mBaseHandler.sendMessage(msg); + } + + public void systemDisconnectDocker() { + Log.i(TAG, "是否存在用户操作=" + isUserAction); + //判断当前是否存在用户操作,如果存在用户操作将不做断开Docker 任务 + if (!isUserAction.get()) + disconnectDocker(false); + } + + public synchronized void disconnectDocker(boolean isNotify) {//需要判断是否是用户打开的Docker 如果是用户打开的将不关闭 + if (mBaseHandler != null) { + Message msg = Message.obtain(); + msg.what = WHAT_CLOSE; + msg.arg1 = isNotify ? 1 : 0; + mBaseHandler.sendMessage(msg); + } else { + stopThread(); + } + } + + + /** + * 开始定时关闭Docker连接 + */ + + public void stopDisconnectTimer() { + if (mBaseHandler != null) { + if (mBaseHandler.hasMessages(WHAT_CLOSE_DELAYED)) + mBaseHandler.removeMessages(WHAT_CLOSE_DELAYED); + } + } + + public void startDisconnectTimer(boolean isNotify) { + stopDisconnectTimer(); + if (mBaseHandler != null) { + Message msg = Message.obtain(); + msg.what = WHAT_CLOSE_DELAYED; + msg.arg1 = isNotify ? 1 : 0; + mBaseHandler.sendMessageDelayed(msg, DEFAULT_CLOSE_TIME); + } + } + + public boolean isDockerOpened() { + return connectStatus.get() == CONNECT_STATUS.CONNECTED; + } + + private void startThread() { + if (mThread == null) { + String[] parts = host.getHostname().split("\\."); + String name = "DockerCmd-" + parts[parts.length - 1]; + mThread = new HandlerThread(name); + mThread.start(); + initHandler(mThread.getLooper()); + + } + } + + private void stopThread() { + isUserAction.set(false); + connectStatus.set(CONNECT_STATUS.CLOSE); + if (mThread != null) { + mThread.quit(); + mThread = null; + } + } + + public void systemExecDockerCommand(String cmd) { + execCommand(cmd, false); + } + + public void execDockerCommand(String cmd, boolean isNotify) { + this.isUserAction.set(true); + execCommand(cmd, isNotify); + } + + private void execCommand(String cmd, boolean isNotify) { + if (mBaseHandler != null) { + Message msg = Message.obtain(); + msg.what = WHAT_EXEC_COMMAND; + msg.arg1 = isNotify ? 1 : 0; + msg.obj = cmd; + mBaseHandler.sendMessage(msg); + } + } + + + /** + * 同Handler 的 handleMessage, + * getHandler.sendMessage,发送的Message在此接收 + * 在此之前确定已经调用initHandler() + * + * @param msg + */ + protected void handleMessage(Message msg) throws IOException { + boolean isNotify = msg.arg1 == 1; + switch (msg.what) { + case WHAT_OPEN: + if (!connection.isAuthenticationComplete()) { + updateConnected(false, isNotify); + } else { + if (session == null) { + session = connection.openSession(); + session.requestDumbPTY(); + session.startShell(); + String command = host.getUsername().equals("dev") ? "linkdocker 2222" : "sudo docker exec -it autocar_default_1 bash"; + // 远端界面返回 + InputStream stdout = session.getStdout(); + // 本地内容推送到远端 + OutputStream stdin = session.getStdin(); + // 写入执行命令 + stdin.write((command + changeLine).getBytes(StandardCharsets.UTF_8)); + // 清空缓存区,开始执行 + stdin.flush(); +// session.waitForCondition(ChannelCondition.EXIT_STATUS, 2000);//session.waitForCondition(ChannelCondition.EXIT_STATUS, 1000);并不会返回执行结果,只会等待超时 + String endResult = readCommandResult(stdout, command);//读取接收到的数据 + Log.i(TAG, host.getHostname() + " 登录Docker,返回的结果为:" + changeLine + endResult); + if (endResult.contains("password")) { + Log.i(TAG, "登录Docker 指令需要密码权限,已自动填充密码"); + // 写入执行命令 + stdin.write((host.getUserPwd() + changeLine).getBytes(StandardCharsets.UTF_8)); + // 清空缓存区,开始执行 + stdin.flush(); + stdin.write(("echo $?" + changeLine).getBytes(StandardCharsets.UTF_8)); + // 清空缓存区,开始执行 + stdin.flush(); + endResult = readCommandResult(stdout, command); + boolean isConnected = TextUtils.equals(endResult, "0"); + updateConnected(isConnected, isNotify); + if (isConnected) { + stdin.write(("export TERM=dumb" + changeLine).getBytes(StandardCharsets.UTF_8));//进入Docker后 修改终端类型 + // 清空缓存区,开始执行 + stdin.flush(); + } + Log.i(TAG, "执行 shell command=" + command + " 输入密码后,返回的结果为:" + changeLine + endResult); + } else { + stdin.write(("echo $?" + changeLine).getBytes(StandardCharsets.UTF_8));//查询命令执行结果 一般非0即失败。session.getExitStatus()这个不生效 + // 清空缓存区,开始执行 + stdin.flush(); + endResult = readCommandResult(stdout, command); + boolean isConnected = TextUtils.equals(endResult, "0"); + updateConnected(isConnected, isNotify); + if (isConnected) { + stdin.write(("export TERM=dumb" + changeLine).getBytes(StandardCharsets.UTF_8));//进入Docker后 修改终端类型 + // 清空缓存区,开始执行 + stdin.flush(); + } + Log.i(TAG, "执行返回值=" + endResult); + } + } + } + break; + case WHAT_CLOSE: + case WHAT_CLOSE_DELAYED: + if (session != null) { + if (isDockerOpened()) { + session.getStdin().write(("exit" + changeLine).getBytes(StandardCharsets.UTF_8)); + } + session.close(); + session = null; + } + if (mBaseHandler != null) { + mBaseHandler.removeCallbacksAndMessages(null); + mBaseHandler = null; + } + stopThread(); + if (connectStatus.get() != CONNECT_STATUS.FAILED) { + connectStatus.set(msg.what == WHAT_CLOSE ? CONNECT_STATUS.CLOSE : CONNECT_STATUS.CLOSE_DELAYED); + listener.onDockerStatus(host, connectStatus.get(), isNotify); + } + break; + case WHAT_EXEC_COMMAND: + String cmd = (String) msg.obj; + String result = null; + if (isDockerOpened() && session != null) { + result = executeCommandInDocker(cmd); + } + listener.onDockerExecResult(host, (String) msg.obj, result, isNotify); + break; + } + } + + private void updateConnected(boolean isConnected, boolean isNotify) { + connectStatus.set(isConnected ? CONNECT_STATUS.CONNECTED : CONNECT_STATUS.FAILED); + if (isUserAction.get()) + isNotify = true; + listener.onDockerStatus(host, connectStatus.get(), isNotify); + if (!isDockerOpened()) { + disconnectDocker(isNotify); + } + } + + private String executeCommandInDocker(String command) throws IOException { + // 远端界面返回 + InputStream stdout = session.getStdout(); + // 本地内容推送到远端 + OutputStream stdin = session.getStdin(); + + // 使用skip()方法跳过所有剩余字节:防止读取到脏数据 + stdout.skip(stdout.available()); + + // 写入执行命令 + stdin.write((command + changeLine).getBytes(StandardCharsets.UTF_8)); + // 清空缓存区,开始执行 + stdin.flush(); + String endResult = readCommandResult(stdout, command); + Log.i(TAG, "执行 shell command=" + command + " 返回的结果为:" + changeLine + endResult); + if (endResult.contains("password")) { + Log.i(TAG, "指令需要密码权限,已自动填充密码"); + return executeCommandInDocker(host.getUserPwd()); + } + if (endResult.isEmpty()) { + return ""; + } + return endResult.trim(); + } + + private String readCommandResult(InputStream inputStream, String command) throws IOException { + StringBuilder builder = new StringBuilder(); + StringBuilder endResult = new StringBuilder(); + String res = ""; + while (!res.endsWith(":~$") && !res.endsWith(":~/") && !res.endsWith(":/#") && !res.endsWith("password for " + host.getUsername() + ":")) { + try { + Thread.sleep(500L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (inputStream.available() > 0) { + //InputStream按位读取,并保存在stringbuffer中 + byte[] bs = new byte[inputStream.available()]; + inputStream.read(bs); + res = new String(bs).trim(); + builder.append(res); + } + } + // 将StringBuff读取的InputStream数据,转换成特定编码格式的字符串,一般为UTF-8格式 + String result = new String(builder.toString().getBytes(StandardCharsets.UTF_8)); + // 将返回结果,按行截取并放进数组里面 + String[] strings = result.split(changeLine); + strings = Arrays.stream(strings) + .filter(s -> !s.isEmpty()) + .toArray(String[]::new); + + // 通过遍历,筛选无意义的字符 + for (int i = 1; i < strings.length; i++) { + String string = strings[i]; + if (!string.contains(command) && + !string.contains(":~$") && + !string.contains(":~/") && + !string.contains(":/#") //&& !strings[i].contains("Last login") + ) { + //获取筛选后的字符 + endResult.append(string.trim()).append(changeLine); + } + } + String temp = endResult.toString(); + // 判断结尾是否是换行符 + if (!TextUtils.isEmpty(temp) && temp.endsWith(changeLine)) { + // 去掉结尾的换行符 + temp = temp.substring(0, temp.length() - 1); + } + return temp; + } + + + /** + * 初始化一个Handler,如果需要使用Handler,先调用此方法, + * 然后可以使用postRunnable(Runnable runnable), + * sendMessage在handleMessage(Message msg)中接收msg + */ + private void initHandler(@NonNull Looper looper) { + mBaseHandler = new BaseHandler(looper, this); + } + + + /** + * 同Handler的postRunnable + * 在此之前确定已经调用initHandler() + */ + public void postRunnable(Runnable runnable) { + postRunnableDelayed(runnable, 0); + } + + /** + * 同Handler的postRunnableDelayed + * 在此之前确定已经调用initHandler() + */ + public void postRunnableDelayed(Runnable runnable, long delayMillis) { + if (mBaseHandler != null) + mBaseHandler.postDelayed(runnable, delayMillis); + } + + + protected static class BaseHandler extends Handler { + private final WeakReference mObjects; + + public BaseHandler(@NonNull Looper looper, DockerCommandHandler mPresenter) { + super(looper); + mObjects = new WeakReference(mPresenter); + } + + + @Override + public void handleMessage(Message msg) { + DockerCommandHandler mPresenter = mObjects.get(); + if (mPresenter != null) { + try { + mPresenter.handleMessage(msg); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/ExecCommandHandler.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/ExecCommandHandler.java new file mode 100644 index 0000000000..c5c33cd034 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/ExecCommandHandler.java @@ -0,0 +1,147 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ssh.module; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnExecCommandListener; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.Session; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; + + +/** + * 命令执行线程 + */ +public class ExecCommandHandler { + private static final String TAG = ExecCommandHandler.class.getSimpleName(); + private static final int BUFFER_SIZE = 1024; + private final OnExecCommandListener listener; + private HandlerThread mThread; + private BaseHandler mBaseHandler; + private final SSHHostBean host; + private Connection connection; + + + public ExecCommandHandler(@NonNull SSHHostBean host, @NonNull Connection connection, @NonNull OnExecCommandListener listener) { + this.host = host; + this.connection = connection; + this.listener = listener; + + } + + public void start() { + if (mThread == null) { + String[] parts = host.getHostname().split("\\."); + String name = "ExecCommand-" + parts[parts.length - 1]; + mThread = new HandlerThread(name); + mThread.start(); + initHandler(mThread.getLooper()); + } + } + + public void stop() { + if (mBaseHandler != null) { + mBaseHandler.removeCallbacksAndMessages(null); + mBaseHandler = null; + } + if (mThread != null) { + mThread.quit(); + mThread = null; + } + } + + public void execCommand(String cmd, boolean isNotify) { + Message msg = Message.obtain(); + msg.obj = cmd; + msg.arg1 = isNotify ? 1 : 0; + mBaseHandler.sendMessage(msg); + } + + + /** + * 同Handler 的 handleMessage, + * getHandler.sendMessage,发送的Message在此接收 + * 在此之前确定已经调用initHandler() + * + * @param msg + */ + protected void handleMessage(Message msg) { + String cmd = (String) msg.obj; + StringBuilder data = new StringBuilder(); + try { + Session session = connection.openSession(); + if (cmd.contains("sudo")) { + cmd = cmd.replace("sudo", "echo " + host.getUserPwd() + " | sudo -S"); + } + InputStream stdout = session.getStdout(); + InputStream stderr = session.getStderr(); + session.execCommand(cmd); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = stdout.read(buffer)) != -1) { + String output = new String(buffer, 0, bytesRead); + data.append(output); + } + session.close(); + } catch (IOException e) { + e.printStackTrace(); + Log.e("SSH", "接收异常", e); + } + listener.onExecResult(host, (String) msg.obj, msg.arg1 == 1, data.toString()); + } + + /** + * 初始化一个Handler,如果需要使用Handler,先调用此方法, + * 然后可以使用postRunnable(Runnable runnable), + * sendMessage在handleMessage(Message msg)中接收msg + */ + private void initHandler(@NonNull Looper looper) { + mBaseHandler = new BaseHandler(looper, this); + } + + + /** + * 同Handler的postRunnable + * 在此之前确定已经调用initHandler() + */ + public void postRunnable(Runnable runnable) { + postRunnableDelayed(runnable, 0); + } + + /** + * 同Handler的postRunnableDelayed + * 在此之前确定已经调用initHandler() + */ + public void postRunnableDelayed(Runnable runnable, long delayMillis) { + if (mBaseHandler != null) + mBaseHandler.postDelayed(runnable, delayMillis); + } + + + protected static class BaseHandler extends Handler { + private final WeakReference mObjects; + + public BaseHandler(@NonNull Looper looper, ExecCommandHandler mPresenter) { + super(looper); + mObjects = new WeakReference(mPresenter); + } + + + @Override + public void handleMessage(Message msg) { + ExecCommandHandler mPresenter = mObjects.get(); + if (mPresenter != null) + mPresenter.handleMessage(msg); + } + } + + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/SSHHostBean.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/SSHHostBean.java new file mode 100644 index 0000000000..6cc08b91f5 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ssh/module/SSHHostBean.java @@ -0,0 +1,304 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * 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. + */ + +package com.zhjt.mogo_core_function_devatools.rviz.ssh.module; + +import android.annotation.SuppressLint; +import android.net.Uri; + +import com.zhjt.mogo_core_function_devatools.rviz.ssh.constant.SSHConstant; + + +/** + * @author Kenny Root + */ +public class SSHHostBean { + + public static final int DEFAULT_FONT_SIZE = 10; + + /* Database fields */ + private long id = -1; + private String nickname = null; + private String username = null; + private String userPwd = null; + private String hostname = null; + private int port = 22; + private long lastConnect = -1; + private String color; + private boolean useKeys = true; + private String useAuthAgent = SSHConstant.AUTHAGENT_NO; + private String postLogin = null; + private boolean wantSession = true; + private String delKey = SSHConstant.DELKEY_DEL; + private int fontSize = DEFAULT_FONT_SIZE; + private boolean compression = false; + private String encoding = SSHConstant.ENCODING_DEFAULT; + private boolean stayConnected = false; + private boolean quickDisconnect = false; + + private boolean isHaveHadMapVersion = false;//此主机是否有地图版本 + + public SSHHostBean() { + + } + + + public SSHHostBean(String nickname, String username, String hostname, int port) { + this.nickname = nickname; + this.username = username; + this.hostname = hostname; + this.port = port; + } + + public void setId(long id) { + this.id = id; + } + + public long getId() { + return id; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getNickname() { + return nickname; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public String getUserPwd() { + return userPwd; + } + + public void setUserPwd(String userPwd) { + this.userPwd = userPwd; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getHostname() { + return hostname; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return port; + } + + + public void setLastConnect(long lastConnect) { + this.lastConnect = lastConnect; + } + + public long getLastConnect() { + return lastConnect; + } + + public void setColor(String color) { + this.color = color; + } + + public String getColor() { + return color; + } + + public void setUseKeys(boolean useKeys) { + this.useKeys = useKeys; + } + + public boolean getUseKeys() { + return useKeys; + } + + public void setUseAuthAgent(String useAuthAgent) { + this.useAuthAgent = useAuthAgent; + } + + public String getUseAuthAgent() { + return useAuthAgent; + } + + public void setPostLogin(String postLogin) { + this.postLogin = postLogin; + } + + public String getPostLogin() { + return postLogin; + } + + public void setWantSession(boolean wantSession) { + this.wantSession = wantSession; + } + + public boolean getWantSession() { + return wantSession; + } + + public void setDelKey(String delKey) { + this.delKey = delKey; + } + + public String getDelKey() { + return delKey; + } + + public void setFontSize(int fontSize) { + this.fontSize = fontSize; + } + + public int getFontSize() { + return fontSize; + } + + public void setCompression(boolean compression) { + this.compression = compression; + } + + public boolean getCompression() { + return compression; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return this.encoding; + } + + public void setStayConnected(boolean stayConnected) { + this.stayConnected = stayConnected; + } + + public boolean getStayConnected() { + return stayConnected; + } + + public void setQuickDisconnect(boolean quickDisconnect) { + this.quickDisconnect = quickDisconnect; + } + + public boolean getQuickDisconnect() { + return quickDisconnect; + } + + public boolean isHaveHadMapVersion() { + return isHaveHadMapVersion; + } + + public void setHaveHadMapVersion(boolean haveHadMapVersion) { + isHaveHadMapVersion = haveHadMapVersion; + } + + @SuppressLint("DefaultLocale") + public String getDescription() { + String description = String.format("%s@%s", username, hostname); + + if (port != 22) + description += String.format(":%d", port); + + return description; + } + + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof SSHHostBean)) + return false; + + SSHHostBean host = (SSHHostBean) o; + + if (nickname == null) { + if (host.getNickname() != null) + return false; + } else if (!nickname.equals(host.getNickname())) + return false; + if (username == null) { + if (host.getUsername() != null) + return false; + } else if (!username.equals(host.getUsername())) + return false; + + if (hostname == null) { + if (host.getHostname() != null) + return false; + } else if (!hostname.equals(host.getHostname())) + return false; + + return port == host.getPort(); + } + + @Override + public int hashCode() { + int hash = 7; + + if (id != -1) + return (int) id; + + hash = 31 * hash + (null == nickname ? 0 : nickname.hashCode()); + hash = 31 * hash + (null == username ? 0 : username.hashCode()); + hash = 31 * hash + (null == hostname ? 0 : hostname.hashCode()); + hash = 31 * hash + port; + + return hash; + } + + /** + * @return URI identifying this HostBean + */ + public Uri getUri() { + StringBuilder sb = new StringBuilder(); + sb.append("ssh://"); + if (username != null) + sb.append(Uri.encode(username)) + .append('@'); + + sb.append(Uri.encode(hostname)) + .append(':') + .append(port) + .append("/#") + .append(nickname); + return Uri.parse(sb.toString()); + } + + /** + * Generates a "pretty" string to be used in the quick-connect host edit view. + */ + @Override + public String toString() { + if (username == null || hostname == null || + username.equals("") || hostname.equals("")) + return ""; + if (port == 22) + return username + "@" + hostname; + else + return username + "@" + hostname + ":" + port; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/activity/FmdAct.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/activity/FmdAct.kt new file mode 100644 index 0000000000..a7e9834d7c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/activity/FmdAct.kt @@ -0,0 +1,335 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.activity + +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import android.os.Looper +import android.text.TextUtils +import android.view.ContextMenu +import android.view.Menu +import android.view.View +import android.widget.Button +import androidx.fragment.app.Fragment +import com.flyco.tablayout.CommonTabLayout +import com.flyco.tablayout.listener.CustomTabEntity +import com.flyco.tablayout.listener.OnTabSelectListener +import com.mogo.eagle.core.utilcode.util.ToastUtils +import com.mogo.eagle.core.utilcode.util.Utils +import com.zhidao.support.adas.high.common.MMKVUtils +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseActivity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.HdMapVersion +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.RosHostArgument +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.TabEntity +import com.zhjt.mogo_core_function_devatools.rviz.service.FaultManagementDiagnosisService +import com.zhjt.mogo_core_function_devatools.rviz.service.FmCodeUpdateService +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean +import com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.fault.FaultCodeFrag +import com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.overview.OverviewFrag +import com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.resource.SystemResourceFrag +import com.zhjt.mogo_core_function_devatools.rviz.ui.views.ColorHintFloatWindowManager +import com.zhjt.mogo_core_function_devatools.rviz.ui.views.StateBarView + +/** + * 故障诊断管理页面 + * 需求来源:http://wiki.zhidaohulian.com/pages/viewpage.action?pageId=121685065 + * + */ + +class FmdAct : BaseActivity() { + private val TAG = "FmdAct" + + private val mFragments = java.util.ArrayList() + + private val mTitles = + arrayOf("车况概览", "系统资源", "故障码") + + private lateinit var overviewFrag: OverviewFrag + private lateinit var faultCodeFrag: FaultCodeFrag + private val mIconUnselectIds = intArrayOf( + R.drawable.rviz_fmd_tab_car_status_unselect, + R.drawable.rviz_fmd_tab_system_resource_unselect, + R.drawable.rviz_fmd_tab_fault_code_unselect, + ) + private val mIconSelectIds = intArrayOf( + R.drawable.rviz_fmd_tab_car_status_select, + R.drawable.rviz_fmd_tab_system_resource_select, + R.drawable.rviz_fmd_tab_fault_code_select, + ) + + private val mTabEntities = ArrayList() + + private var serviceIsBind = false + private var fmdBound: FaultManagementDiagnosisService? = null//FMD服务 + + private var colorHintFloatWindowManager: ColorHintFloatWindowManager? = null + + private lateinit var clConnectStatusBarView: StateBarView + private lateinit var tbCheckTypeView: CommonTabLayout + private lateinit var btnColorHint: Button + private lateinit var btnClose: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.rviz_fmd_act_home) + clConnectStatusBarView = findViewById(R.id.clConnectStatusBarView) + tbCheckTypeView = findViewById(R.id.tbCheckTypeView) + btnColorHint = findViewById(R.id.btnColorHint) + btnClose = findViewById(R.id.btnClose) + + MMKVUtils.getInstance().init(applicationContext) + // 启动数据库更新服务 + Utils.getApp().startService(Intent(Utils.getApp(), FmCodeUpdateService::class.java)) + registerForContextMenu(clConnectStatusBarView.vehicleNumberView) + overviewFrag = OverviewFrag() + faultCodeFrag = FaultCodeFrag() + mFragments.add(overviewFrag) + mFragments.add(SystemResourceFrag()) + mFragments.add(faultCodeFrag) + + for (i in mTitles.indices) { + mTabEntities.add(TabEntity(mTitles[i], mIconSelectIds[i], mIconUnselectIds[i])) + } + tbCheckTypeView.setTabData( + mTabEntities, + this, + R.id.frameContainerLayout, + mFragments + ) + tbCheckTypeView.setOnTabSelectListener(object : OnTabSelectListener { + override fun onTabSelect(position: Int) { + btnColorHint.visibility = + if (mFragments[position] is OverviewFrag || mFragments[position] is FaultCodeFrag) { + if (colorHintFloatWindowManager != null) { + colorHintFloatWindowManager!!.updateSshConnectErrorView(mFragments[position] is OverviewFrag) + } + View.VISIBLE + } else { + removeColorHintFloatWindow() + View.GONE + } + } + + override fun onTabReselect(position: Int) { + + } + }) + + //显示未读红点 + +// tbCheckTypeView.setMsgMargin(0, -5f, 5f); + btnColorHint.setOnClickListener { + if (colorHintFloatWindowManager == null) { + colorHintFloatWindowManager = ColorHintFloatWindowManager() + colorHintFloatWindowManager!!.show(this, !overviewFrag.isHidden) + btnColorHint.isSelected = true + } else { + removeColorHintFloatWindow() + } + } + + btnClose.setOnClickListener { + // TODO 这里需要对已经开启的后台检测任务及线程做关闭操作 + + finish() + } + if (!serviceIsBind) { + // 绑定连接服务 + bindService( + Intent(this, FaultManagementDiagnosisService::class.java), + fmdConnection, + BIND_AUTO_CREATE + ) + serviceIsBind = true + } + } + + fun updateRedDot(tag: String, num: Int) { + var pos = -1 + when (tag) { + OverviewFrag::class.java.simpleName -> { + pos = 0 + } + + SystemResourceFrag::class.java.simpleName -> { + pos = 1 + } + + FaultCodeFrag::class.java.simpleName -> { + pos = 2 + } + } + if (pos != -1) { + if (Looper.myLooper() != Looper.getMainLooper()) { + runOnUiThread { + redDot(pos, num) + } + } else { + redDot(pos, num) + } + } + } + + private fun redDot(pos: Int, num: Int) { + if (num <= 0) { + tbCheckTypeView.hideMsg(pos) + } else { + tbCheckTypeView.showMsg(pos, num); + } + } + + override fun onCreateContextMenu( + menu: ContextMenu?, + v: View?, + menuInfo: ContextMenu.ContextMenuInfo? + ) { + super.onCreateContextMenu(menu, v, menuInfo) + if (v?.id == R.id.vehicle_number_view) { + if (fmdBound != null && fmdBound!!.getVehicleConfig() != null) { + menu?.setHeaderTitle("车辆基础信息") + val config = fmdBound!!.getVehicleConfig() + menu?.add(Menu.NONE, 3, Menu.NONE, "车牌号码:${config!!.plate}") + menu?.add(Menu.NONE, 2, Menu.NONE, "车辆品牌:${config!!.brand}") + menu?.add(Menu.NONE, 1, Menu.NONE, "车辆类型:${config!!.model}") + } + } + } + + private val fmdConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + fmdBound = + (service as FaultManagementDiagnosisService.FaultManagementDiagnosisBinder).service + faultCodeFrag.setData(fmdBound!!.fmDataMap) + overviewFrag.setData(fmdBound!!.fmDataMap) + fmdBound!!.startDiagnose() + } + + override fun onServiceDisconnected(className: ComponentName) { + fmdBound = null + } + } + + fun getRosHostArguments(): ArrayList? { + return if (fmdBound == null) { + ToastUtils.showLong("服务绑定失败,请退出页面重试") + null + } else { + fmdBound!!.rosHostArguments + } + } + + fun getFmEntityData(): MutableMap? { + return if (fmdBound == null) { + null + } else { + fmdBound!!.fmDataMap + } + } + + /** + * 获取102 MAP 版本号 + */ + fun getRosMasterMapVersion(): String { + return if (fmdBound == null) { + "未知" + } else { + fmdBound!!.getRosMasterMapVersion() + } + } + + /** + * 获取高精地图版本 + */ + fun getHdMapVersion(): HdMapVersion? { + return if (fmdBound == null) { + null + } else { + fmdBound!!.getHdMapVersion() + } + } + + /** + * 获取云端MAP版本号 + */ + fun getCloudMapVersion(): String { + return if (fmdBound == null) { + "未知" + } else { + fmdBound!!.getCloudMapVersion() + } + } + + public fun getSSH(hostBean: SSHHostBean): SSH? { + if (fmdBound != null) { + val bridge = fmdBound!!.getConnectedSSH(hostBean) + if (bridge != null) { + return bridge + } + } + ToastUtils.showLong("终端服务未绑定或绑定失败") + return null + } + + public fun openConnection(hostBean: SSHHostBean) { + if (fmdBound != null) { + fmdBound!!.openConnection(hostBean) + return + } + ToastUtils.showLong("终端服务未绑定或绑定失败") + } + + public fun execCmd(hostBean: SSHHostBean, cmd: String, msg: String?) { + val bridge = getSSH(hostBean); + if (bridge != null) { + execCmd(bridge, cmd, msg); + } + } + + public fun execCmd(ssh: SSH, cmd: String, msg: String?) { + if (TextUtils.isEmpty(msg)) { + showLoadingDialog() + } else { + showLoadingDialog(msg) + } + ssh.execCommand(cmd, true) + } + + public fun execDockerCmd(hostBean: SSHHostBean, cmd: String) { + getSSH(hostBean)?.execDockerCommand(cmd, true) + } + + public fun connectDocker(ssh: SSH) { + showLoadingDialog("正在连接Docker") + ssh.connectDocker() + } + + public fun disconnectDocker(ssh: SSH) { + ssh.disconnectDocker(true) + } + + private fun removeColorHintFloatWindow() { + colorHintFloatWindowManager?.remove() + colorHintFloatWindowManager = null + btnColorHint.isSelected = false + } + + override fun onPause() { + super.onPause() + removeColorHintFloatWindow() + } + + override fun onDestroy() { + super.onDestroy() + if (serviceIsBind) { + unbindService(fmdConnection) + serviceIsBind = false + } + + } + + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/FmdBaseFragment.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/FmdBaseFragment.kt new file mode 100644 index 0000000000..affc7febcd --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/FmdBaseFragment.kt @@ -0,0 +1,21 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.fragments + +import android.os.Bundle +import android.view.View +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseFragment +import com.zhjt.mogo_core_function_devatools.rviz.ui.activity.FmdAct + +abstract class FmdBaseFragment : BaseFragment() { + protected lateinit var fmdAct: FmdAct + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (requireActivity() is FmdAct) { + fmdAct = requireActivity() as FmdAct + } + } + + public fun getTAG(): String { + return TAG + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeAdapter.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeAdapter.kt new file mode 100644 index 0000000000..9446ada06c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeAdapter.kt @@ -0,0 +1,136 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.fault + +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.mogo.eagle.core.utilcode.util.ActivityUtils +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseAdapter +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseViewHolder +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultLevel +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultSubModuleId +import com.zhjt.mogo_core_function_devatools.rviz.dialog.FaultCodeDetailsDialog +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmEntity + + +class FaultCodeAdapter : BaseAdapter() { + var isConnected: Boolean = false//域控是否连接成功 + + + override fun onBindDataToItem(viewHolder: ViewHolder, data: FmEntity, position: Int) { + viewHolder.tvModuleTitle.text = data.title + if (isConnected) { + val isNormal = data.stopFaultNum + data.otherFaultNum + data.unknownFaultNum == 0 + viewHolder.ivNormal.isSelected = false + viewHolder.ivNormal.visibility = if (isNormal) View.VISIBLE else View.GONE + viewHolder.btn.visibility = if (isNormal) View.GONE else View.VISIBLE + viewHolder.tvModuleMsg.visibility = if (isNormal) View.GONE else View.VISIBLE + viewHolder.tvModuleStateNormal.visibility = + if (isNormal) View.VISIBLE else View.GONE + viewHolder.tvStopFaultNum.visibility = + if (isNormal) View.GONE else View.VISIBLE + viewHolder.tvOtherFaultNum.visibility = + if (isNormal) View.GONE else View.VISIBLE + viewHolder.tvUnknownFaultNum.visibility = + if (isNormal) View.GONE else if (data.unknownFaultNum == 0) View.GONE else View.VISIBLE + if (isNormal) { + viewHolder.tvModuleMsg.text = "" + } else { + viewHolder.tvStopFaultNum.text = + String.format("严重故障:%s个", data.stopFaultNum) + viewHolder.tvOtherFaultNum.text = + String.format("其他故障:%s个", data.otherFaultNum) + viewHolder.tvUnknownFaultNum.text = + String.format("未知故障:%s个", data.unknownFaultNum) + + val list = data.data.take(4) + val builder = SpannableStringBuilder() + for (i in list.indices) { + val info = list[i] + val start = builder.length // 当前文本的起始位置 + val subName = FaultSubModuleId.getName(info.faultId, true) +// builder.append("${i + 1}、${subName}${info.faultName}\n") + builder.append("${subName}${info.faultName}\n") + val end = builder.length // 当前文本的结束位置 + builder.setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + mContext, + FaultLevel.getColor(info.policyCode, info.faultLevel) + ) + ), + start, end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + if (data.data.size > 4) { + val start = builder.length + builder.append("......") + builder.setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + mContext, + R.color.rviz_fmd_fm_fault_level_unknown + ) + ), // 设置省略号的颜色 + start, builder.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + if (builder.endsWith("\n")) { + builder.delete(builder.length - 1, builder.length) + } + viewHolder.tvModuleMsg.text = builder + } + } else { + viewHolder.tvModuleMsg.text = "" + viewHolder.ivNormal.isSelected = true + viewHolder.ivNormal.visibility = View.VISIBLE + viewHolder.tvModuleStateNormal.visibility = View.GONE + viewHolder.btn.visibility = View.GONE + viewHolder.tvStopFaultNum.visibility = View.GONE + viewHolder.tvOtherFaultNum.visibility = View.GONE + viewHolder.tvUnknownFaultNum.visibility = View.GONE + + } + } + + override fun getItemViewResource(viewGroup: ViewGroup?): View { + return LayoutInflater.from(mContext) + .inflate(R.layout.rviz_fmd_item_fault_code, viewGroup, false); + } + + override fun getViewHolder(view: View): ViewHolder { + return ViewHolder(view, this) + } + + + inner class ViewHolder(itemView: View, adapter: FaultCodeAdapter) : + BaseViewHolder(itemView, adapter) { + var btn: TextView = itemView.findViewById(R.id.btn) + var tvModuleTitle: TextView = itemView.findViewById(R.id.tvModuleTitle) + var ivNormal: ImageView = itemView.findViewById(R.id.ivNormal) + var tvModuleMsg: TextView = itemView.findViewById(R.id.tvModuleMsg) + var tvModuleStateNormal: TextView = itemView.findViewById(R.id.tvModuleStateNormal) + var tvStopFaultNum: TextView = itemView.findViewById(R.id.tvStopFaultNum) + var tvOtherFaultNum: TextView = itemView.findViewById(R.id.tvOtherFaultNum) + var tvUnknownFaultNum: TextView = itemView.findViewById(R.id.tvUnknownFaultNum) + + init { + btn.setOnClickListener { + ActivityUtils.getTopActivity()?.let { + FaultCodeDetailsDialog( + it, + mDatas[bindingAdapterPosition] + ).show() + } + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeFrag.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeFrag.kt new file mode 100644 index 0000000000..bc0ce780b3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/fault/FaultCodeFrag.kt @@ -0,0 +1,138 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.fault + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import com.zhjt.mogo.adas.data.AdasConstants.IpcConnectionStatus +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus +import com.zhjt.mogo_core_function_devatools.rviz.constant.AppConfigInfo +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.AdasConnectionStatus +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmEntity +import com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.FmdBaseFragment + +/** + * 故障码 UI及业务逻辑 + */ +class FaultCodeFrag : FmdBaseFragment() { + private var isInit = false + private var faultCodeAdapter: FaultCodeAdapter? = null + private lateinit var recyclerView: RecyclerView + private lateinit var hintView: TextView + private lateinit var hint1View: TextView + + override fun getContentViewResource( + inflater: LayoutInflater, + container: ViewGroup? + ): View { + return inflater.inflate(R.layout.rviz_fmd_frag_fault_code, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recyclerView = view.findViewById(R.id.recyclerView) + hintView = view.findViewById(R.id.hint) + hint1View = view.findViewById(R.id.hint1) + val animator = recyclerView.itemAnimator + if (animator is SimpleItemAnimator) { + animator.supportsChangeAnimations = + false + } + val linearLayoutManager = GridLayoutManager(context, 3) + linearLayoutManager.orientation = GridLayoutManager.VERTICAL + recyclerView.layoutManager = linearLayoutManager + faultCodeAdapter = FaultCodeAdapter() + recyclerView.adapter = faultCodeAdapter + initFlowBusEvent() + } + + private fun initFlowBusEvent() { + FlowBus.with(EventKey.UPDATE_FAULT_CODE_DATA) + .register(this) { + if (it < 0) { + faultCodeAdapter?.notifyDataSetChanged() + } else { + faultCodeAdapter?.notifyItemChanged(it) + } + updateRedDot() + } + FlowBus.with(EventKey.SEND_IS_SUPPORT_FM) + .register(this) { + if (!it) { + notSupportFM() + } + } + FlowBus.with(EventKey.UPDATE_ADAS_CONNECT_STATE) + .register(this) { it: AdasConnectionStatus -> + val status = it.ipcConnectionStatus + if (status == IpcConnectionStatus.CONNECTED) { + faultCodeAdapter?.isConnected = true + } else if (status == IpcConnectionStatus.DISCONNECTED || + status == IpcConnectionStatus.CONNECT_EXCEPTION || + status == IpcConnectionStatus.ILLEGAL_ADDRESS || + status == IpcConnectionStatus.NOT_FOUND_ADDRESS || + status == IpcConnectionStatus.CERTIFICATION_FAILED || + status == IpcConnectionStatus.HEARTBEAT_TIMEOUT || + status == IpcConnectionStatus.PROTOCOL_MISMATCH || + status == IpcConnectionStatus.SERVER_DISCONNECTED + ) { + faultCodeAdapter?.isConnected = false + } + + } + } + + public fun setData(data: MutableMap?) { + if (!isInit) { + isInit = true + if (data == null) { + hintView.visibility = View.VISIBLE + } else { + faultCodeAdapter?.data = data.values.toList() + updateRedDot() + } + } + } + + private fun initData() { + if (!isInit) { + isInit = true + val data = fmdAct.getFmEntityData() + if (data == null) { + hintView.visibility = View.VISIBLE + } else { + faultCodeAdapter?.data = data.values.toList() + updateRedDot() + } + } + } + + private fun notSupportFM() { + recyclerView.visibility = View.GONE + hintView.text = "FM相关功能要求至少MAP版本3.6.0或更高版本的支持" + context?.getColor(R.color.rviz_fmd_status_error)?.let { hintView.setTextColor(it) } + hintView.visibility = View.VISIBLE + hint1View.text = AppConfigInfo.dockerVersion + "\n" + AppConfigInfo.mapVersion + hint1View.visibility = View.VISIBLE + } + + private fun updateRedDot() { + val totalSum = faultCodeAdapter?.data?.sumOf { + it.data.size + } ?: 0 + fmdAct.updateRedDot(TAG, totalSum) + } + + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (!hidden) { + initData() + } + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/ModuleStatusAdapter.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/ModuleStatusAdapter.java new file mode 100644 index 0000000000..c257f54077 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/ModuleStatusAdapter.java @@ -0,0 +1,61 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.overview; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.ModuleStatusEntity; +import com.zhjt.mogo_core_function_devatools.rviz.ui.views.SensorStatusView; + +import java.util.ArrayList; + +/** + * 车辆模块检测结果列表 + */ +public class ModuleStatusAdapter extends RecyclerView.Adapter { + + private ArrayList mModuleStatusEntity = null; + + public void updateModuleStatus(ArrayList moduleStatusEntity) { + if (mModuleStatusEntity != null) { + mModuleStatusEntity.clear(); + mModuleStatusEntity = null; + } + mModuleStatusEntity = moduleStatusEntity; + notifyDataSetChanged(); + } + + @NonNull + @Override + public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + SensorStatusView itemView = new SensorStatusView(parent.getContext()); + //添加LayoutParams,避免崩溃 + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + itemView.setLayoutParams(params); + + return new MyViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { + ModuleStatusEntity moduleStatus = mModuleStatusEntity.get(position); + holder.sensorStatusView.updateSensors(moduleStatus); + } + + @Override + public int getItemCount() { + return mModuleStatusEntity.size(); + } + + public static class MyViewHolder extends RecyclerView.ViewHolder { + public SensorStatusView sensorStatusView; + + public MyViewHolder(SensorStatusView view) { + super(view); + sensorStatusView = view; + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/OverviewFrag.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/OverviewFrag.kt new file mode 100644 index 0000000000..f8ae88c355 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/overview/OverviewFrag.kt @@ -0,0 +1,884 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.overview + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.text.SpannableString +import android.text.Spanned +import android.text.TextUtils +import android.text.method.ScrollingMovementMethod +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotActionsListener +import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotActionsListenerManager +import com.mogo.eagle.core.utilcode.util.AppUtils +import com.zhjt.mogo.adas.data.AdasConstants.IpcConnectionStatus +import com.zhjt.mogo.adas.data.bean.LaunchConditionData +import com.zhjt.mogo.adas.data.bean.UnableLaunchReason +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.Utils +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultLevel +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultModuleId +import com.zhjt.mogo_core_function_devatools.rviz.constant.SensorCamera +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.AdasConnectionStatus +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FMInfoMsg +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.FmEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.HdMapVersion +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.ModuleStatusEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.SensorStatusEntity +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.call.CallerSshConnectionListenerManager +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnSshConnectionListener +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean +import com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.FmdBaseFragment +import fault_management.FmInfo +import kotlinx.android.synthetic.main.rviz_fmd_frag_overview.tvHdMapVersion +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Locale + + +/** + * 车况概览 UI及业务逻辑 + */ +class OverviewFrag : FmdBaseFragment(), OnSshConnectionListener, + IMoGoAutopilotActionsListener { + private var lastOpenedAdapter: ModuleStatusAdapter? = null + private lateinit var spanError: ForegroundColorSpan + private val mModuleStatusEntity = ArrayList() + private var fmDataMap: MutableMap? = null//FM分类数据源 + private val sensorStatusNormalLog = + arrayListOf(SensorStatusEntity("无异常", true)) + + @Volatile + private var fmInfoMsg: FMInfoMsg? = null + private lateinit var packageManagerReceiver: PackageManagerReceiver + private lateinit var tvAutoDriveMsg: TextView + private lateinit var tvMapVersion: TextView + private lateinit var tvEagleVersion: TextView + private lateinit var tvAutoDriveStatus: TextView + private lateinit var rvModuleStatus: RecyclerView + + + override fun getContentViewResource( + inflater: LayoutInflater, + container: ViewGroup? + ): View { + return inflater.inflate(R.layout.rviz_fmd_frag_overview, container, false) + } + + override fun onDestroyView() { + super.onDestroyView() + CallerSshConnectionListenerManager.removeListener(TAG) + CallerAutopilotActionsListenerManager.removeListener(TAG) + context?.unregisterReceiver(packageManagerReceiver) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + tvAutoDriveMsg = view.findViewById(R.id.tvAutoDriveMsg) + rvModuleStatus = view.findViewById(R.id.rvModuleStatus) + tvMapVersion = view.findViewById(R.id.tvMapVersion) + tvEagleVersion = view.findViewById(R.id.tvEagleVersion) + tvAutoDriveStatus = view.findViewById(R.id.tvAutoDriveStatus) + tvAutoDriveMsg.movementMethod = ScrollingMovementMethod.getInstance(); + CallerSshConnectionListenerManager.addListener(TAG, this) + CallerAutopilotActionsListenerManager.addListener(TAG, this) + spanError = ForegroundColorSpan(resources.getColor(R.color.rviz_fmd_status_error, null)) + // 取消动画 + val animator = rvModuleStatus.itemAnimator + if (animator is SimpleItemAnimator) { + animator.supportsChangeAnimations = + false + } + initFlowBusEvent() +// // TODO 模拟传感器错误 +// val sensorStatus = ArrayList() +// sensorStatus.add(SensorStatusEntity("102", true)) +// sensorStatus.add(SensorStatusEntity("103", true)) +// sensorStatus.add(SensorStatusEntity("104", false)) +// sensorStatus.add(SensorStatusEntity("105", true)) +// sensorStatus.add(SensorStatusEntity("106", true)) +// sensorStatus.add(SensorStatusEntity("107", true)) +// val carSensorStatusEntity = +// ModuleStatusEntity("域控主机", R.drawable.icon_camera, sensorStatus, false) +// +// +// // 模拟日志错误 +// val sensorStatusLog = ArrayList() +// sensorStatusLog.add(SensorStatusEntity("惯导初始化失败", false)) +// sensorStatusLog.add(SensorStatusEntity("惯导数据延迟超过阈值", false)) +// val carSensorStatusEntityLog = +// ModuleStatusEntity("底盘状态", R.drawable.icon_camera, sensorStatusLog, true) + mModuleStatusEntity.add( + ModuleStatusEntity( + getString(R.string.rviz_fmd_module_host), + R.drawable.rviz_fmd_icon_ipc, + false + ) + ) + mModuleStatusEntity.add( + ModuleStatusEntity( + getString(R.string.rviz_fmd_module_chassis), + R.drawable.rviz_fmd_icon_car_chassis, + true + ) + ) + mModuleStatusEntity.add( + ModuleStatusEntity( + getString(R.string.rviz_fmd_module_rtk), + R.drawable.rviz_fmd_icon_rtk, + true + ) + ) + mModuleStatusEntity.add( + ModuleStatusEntity( + getString(R.string.rviz_fmd_module_camera), + R.drawable.rviz_fmd_icon_camera, + false + ) + ) + mModuleStatusEntity.add( + ModuleStatusEntity( + getString(R.string.rviz_fmd_module_laser_radar), + R.drawable.rviz_fmd_icon_laser_radar, + true + ) + ) + mModuleStatusEntity.add( + ModuleStatusEntity( + getString(R.string.rviz_fmd_module_millimeter_wave_radar), + R.drawable.rviz_fmd_icon_millimeter_wave_radar, + true + ) + ) + + lastOpenedAdapter = ModuleStatusAdapter() + lastOpenedAdapter?.updateModuleStatus(mModuleStatusEntity) + rvModuleStatus.adapter = lastOpenedAdapter + setText( + tvMapVersion, + String.format("102 MAP版本:%s", fmdAct.getRosMasterMapVersion()) + ) + val argument = fmdAct.getHdMapVersion() + if (argument != null) { + setText( + tvHdMapVersion, String.format( + "%s 高精地图版本:%s", + Utils.getIPLastSegment(argument.ip), + argument.version + ) + ) + } else { + setText( + tvHdMapVersion, + "10X 高精地图版本:未知", + ) + } + updateAutopilotAbility() + getMoGoEagleEyeVersion() + packageManagerReceiver = PackageManagerReceiver() + val filter = IntentFilter() + filter.addAction(Intent.ACTION_PACKAGE_ADDED) + filter.addAction(Intent.ACTION_PACKAGE_REPLACED) + filter.addAction(Intent.ACTION_PACKAGE_REMOVED) + filter.addDataScheme("package") + context?.registerReceiver(packageManagerReceiver, filter) + } + + override fun onResume() { + super.onResume() + getMoGoEagleEyeVersion() + } + + //获取鹰眼版本 + @OptIn(DelicateCoroutinesApi::class) + private fun getMoGoEagleEyeVersion() { + GlobalScope.launch(Dispatchers.IO) { + val verName = AppUtils.getAppVersionName("com.mogo.launcher.f") + val msg = if (!TextUtils.isEmpty(verName)) { + String.format("当前设备鹰眼版本:%s", verName) + } else { + "当前设备鹰眼版本:未安装" + } + withContext(Dispatchers.Main) { + setText(tvEagleVersion, msg) + } + } + } + + + inner class PackageManagerReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.action?.let { + when (it) { + Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_REMOVED -> { + val data = intent.data + if (data != null) { + val packageName = data.encodedSchemeSpecificPart ?: return + if ("com.mogo.launcher.f" == packageName) { + getMoGoEagleEyeVersion() + } + } + } + + else -> {} + } + } + } + } + + @OptIn(DelicateCoroutinesApi::class) + private fun updateAutopilotAbility() { + val color: Int + val msg = if (CallerAutopilotActionsListenerManager.isAutopilotAbility()) { + color = R.color.rvizFmdColorBlock + "可以启动「鹰眼」进行运营" + } else { + color = R.color.rviz_fmd_status_error + val resultString = StringBuilder() + CallerAutopilotActionsListenerManager.getUnableAutopilotReasons() + ?.mapIndexed { index, reason -> + resultString.append("${index + 1}. ${reason.unableLaunchReason} ") + } + resultString.toString() + } + GlobalScope.launch(Dispatchers.Main) { + tvAutoDriveStatus.isSelected = + CallerAutopilotActionsListenerManager.isAutopilotAbility() + tvAutoDriveStatus.text = + if (CallerAutopilotActionsListenerManager.isAutopilotAbility()) "可自动驾驶" else "不可自动驾驶" + tvAutoDriveMsg.text = msg + tvAutoDriveMsg.setTextColor(resources.getColor(color, null)) + tvAutoDriveStatus.setTextColor(resources.getColor(color, null)) + } + } + + private fun initFlowBusEvent() { + FlowBus.with(EventKey.SEND_ROS_MASTER_MAP_VERSION) + .register(this) { + setText(tvMapVersion, String.format("102 MAP版本:%s", it)) + } + FlowBus.with(EventKey.SEND_HD_MAP_VERSION) + .register(this) { + setText( + tvHdMapVersion, String.format( + "%s 高精地图版本:%s", + Utils.getIPLastSegment(it.ip), + it.version + ) + ) + } + + FlowBus.with>(EventKey.INIT_SENSOR_CAMERA) + .register(this) { + updateCamera(it) + } + FlowBus.with(EventKey.SEND_IS_SUPPORT_FM) + .register(this) { + if (!it) { + val moduleChassis = + mModuleStatusEntity.indexOfFirst { mt -> + mt.title == getString(R.string.rviz_fmd_module_chassis) + } + if (moduleChassis != -1) { + mModuleStatusEntity.removeAt(moduleChassis) + lastOpenedAdapter?.notifyItemRemoved(moduleChassis) + } + + val moduleRtk = + mModuleStatusEntity.indexOfFirst { mt -> + mt.title == getString(R.string.rviz_fmd_module_rtk) + } + if (moduleRtk != -1) { + mModuleStatusEntity.removeAt(moduleRtk) + lastOpenedAdapter?.notifyItemRemoved(moduleRtk) + } + + val moduleCamera = + mModuleStatusEntity.indexOfFirst { mt -> + mt.title == getString(R.string.rviz_fmd_module_camera) + } + if (moduleCamera != -1) { + mModuleStatusEntity.removeAt(moduleCamera) + lastOpenedAdapter?.notifyItemRemoved(moduleCamera) + } + + val moduleLaserRadar = + mModuleStatusEntity.indexOfFirst { mt -> + mt.title == getString(R.string.rviz_fmd_module_laser_radar) + } + if (moduleLaserRadar != -1) { + mModuleStatusEntity.removeAt(moduleLaserRadar) + lastOpenedAdapter?.notifyItemRemoved(moduleLaserRadar) + } + + val moduleMillimeterWaveRadar = + mModuleStatusEntity.indexOfFirst { mt -> + mt.title == getString(R.string.rviz_fmd_module_millimeter_wave_radar) + } + if (moduleMillimeterWaveRadar != -1) { + mModuleStatusEntity.removeAt(moduleMillimeterWaveRadar) + lastOpenedAdapter?.notifyItemRemoved(moduleMillimeterWaveRadar) + } + } + } + FlowBus.with(EventKey.UPDATE_ADAS_CONNECT_STATE) + .register(this) { + fmInfoMsg = null + val ipcConnectionStatus: IpcConnectionStatus = it.ipcConnectionStatus + if (ipcConnectionStatus == IpcConnectionStatus.CONNECTED) { + //底盘无异常 + updateItem(getString(R.string.rviz_fmd_module_chassis), sensorStatusNormalLog) + //RTK无异常 + updateItem(getString(R.string.rviz_fmd_module_rtk), sensorStatusNormalLog) + //激光雷达无异常 + updateItem( + getString(R.string.rviz_fmd_module_laser_radar), + sensorStatusNormalLog + ) + //毫米波雷达无异常 + updateItem( + getString(R.string.rviz_fmd_module_millimeter_wave_radar), + sensorStatusNormalLog + ) + } else if (ipcConnectionStatus == IpcConnectionStatus.DISCONNECTED) { + updateItem(getString(R.string.rviz_fmd_module_chassis), arrayListOf()) + updateItem(getString(R.string.rviz_fmd_module_rtk), arrayListOf()) + updateItem(getString(R.string.rviz_fmd_module_laser_radar), arrayListOf()) + updateItem( + getString(R.string.rviz_fmd_module_millimeter_wave_radar), + arrayListOf() + ) + } + } + FlowBus.with(EventKey.SEND_FM_INFO_TO_OVERVIEW_FRAGMENT) + .register(this) { + fmInfoMsg = it + //判断是否有相机异常 + updateCamera(null) + if (it.fmInfoList.isNullOrEmpty()) { + //证明FM没有任何异常数据,所有鱼FM相关的全部正常 + //底盘无异常 + updateItem(getString(R.string.rviz_fmd_module_chassis), sensorStatusNormalLog) + //RTK无异常 + updateItem(getString(R.string.rviz_fmd_module_rtk), sensorStatusNormalLog) + + //激光雷达无异常 + updateItem( + getString(R.string.rviz_fmd_module_laser_radar), + sensorStatusNormalLog + ) + //毫米波雷达无异常 + updateItem( + getString(R.string.rviz_fmd_module_millimeter_wave_radar), + sensorStatusNormalLog, + true + ) + } else { + //判断是否有底盘异常 + val vehicleControlList = it.fmInfoList!!.filter { t -> + t.faultId.lowercase(Locale.getDefault()) + .startsWith(FaultModuleId.VehicleControl.name.lowercase(Locale.getDefault())) + }.take(2) + if (vehicleControlList.isEmpty()) { + //无异常 + updateItem( + getString(R.string.rviz_fmd_module_chassis), + sensorStatusNormalLog + ) + } else { + //有异常 + val sensorError = ArrayList(vehicleControlList.map { vcl -> + SensorStatusEntity( + vcl.faultName, + false, + FaultLevel.getColor(vcl.policyCode, vcl.faultLevel) + ) + }) + updateItem(getString(R.string.rviz_fmd_module_chassis), sensorError) + } + + //判断是否有RTK异常 + val rtkList = it.fmInfoList!!.filter { t -> +// t.faultId.lowercase(Locale.getDefault()) +// .startsWith( +// (FaultModuleId.Localization.name + "_MSFLOC").lowercase( +// Locale.getDefault() +// ) +// ) + //只要包含 宽泛匹配 + t.faultId.contains(("MSFLOC")) + }.take(2) + if (rtkList.isEmpty()) { + //无异常 + updateItem(getString(R.string.rviz_fmd_module_rtk), sensorStatusNormalLog) + } else { + //有异常 + val sensorError = ArrayList(rtkList.map { vcl -> + SensorStatusEntity( + vcl.faultName, + false, + FaultLevel.getColor(vcl.policyCode, vcl.faultLevel) + ) + }) + updateItem(getString(R.string.rviz_fmd_module_rtk), sensorError) + } + + + //判断是否有激光雷达异常 + val laserRadarList = it.fmInfoList!!.filter { t -> + //只要包含 宽泛匹配 + t.faultId.contains(("Lidar")) + }.take(2) + if (laserRadarList.isEmpty()) { + //无异常 + updateItem( + getString(R.string.rviz_fmd_module_laser_radar), + sensorStatusNormalLog + ) + } else { + //有异常 + val sensorError = ArrayList(laserRadarList.map { vcl -> + SensorStatusEntity( + vcl.faultName, + false, + FaultLevel.getColor(vcl.policyCode, vcl.faultLevel) + ) + }) + updateItem(getString(R.string.rviz_fmd_module_laser_radar), sensorError) + } + + //判断是否有毫米波雷达异常 + val millimeterWaveRadarList = it.fmInfoList!!.filter { t -> + //只要包含 宽泛匹配 + t.faultId.contains(("Radar")) + }.take(2) + if (millimeterWaveRadarList.isEmpty()) { + //无异常 + updateItem( + getString(R.string.rviz_fmd_module_millimeter_wave_radar), + sensorStatusNormalLog + ) + } else { + //有异常 + val sensorError = ArrayList(millimeterWaveRadarList.map { vcl -> + SensorStatusEntity( + vcl.faultName, + false, + FaultLevel.getColor(vcl.policyCode, vcl.faultLevel) + ) + }) + updateItem( + getString(R.string.rviz_fmd_module_millimeter_wave_radar), + sensorError + ) + } + } + updateRedDot() + } + } + + //判断是否有相机异常 + private fun updateCamera(it: ArrayList?) { + val moduleCamera = getString(R.string.rviz_fmd_module_camera) + val index = mModuleStatusEntity.indexOfFirst { it.title == moduleCamera } + if (index != -1) { + val moduleStatusEntity = mModuleStatusEntity[index] + if (moduleStatusEntity.sensorList.isEmpty()) { + if (it != null) { + moduleStatusEntity.sensorList = it + } + } + if (moduleStatusEntity.sensorList.isNotEmpty()) { + if (fmInfoMsg == null || fmInfoMsg!!.fmInfoList == null) { + //FM无异常数据 + moduleStatusEntity.sensorList.forEach { + it.sensorIsOk = true + it.notOkColorRes = -1 + } + } else { + var cameraFaultList = fmInfoMsg!!.fmInfoList!!.filter { t -> + t.faultId.lowercase(Locale.getDefault()) + .startsWith( + (FaultModuleId.HardwareDriver.name + "_DriversCamera").lowercase( + Locale.getDefault() + ) + ) + } + if (cameraFaultList.isEmpty()) { + //FM存在异常,但是无相机相关异常 + moduleStatusEntity.sensorList.forEach { + it.sensorIsOk = true + it.notOkColorRes = -1 + } + } else { + cameraFaultList = + cameraFaultList.sortedWith(compareByDescending { FaultLevel.getOrder(it.policyCode) })//排序,等级高的显示在最上方 + //判断哪个相机有异常 + updateCameraItemData( + moduleStatusEntity, + cameraFaultList, + SensorCamera.DriversCameraSensing30 + )// 前左30 + updateCameraItemData( + moduleStatusEntity, + cameraFaultList, + SensorCamera.DriversCameraSensing60 + )// 前中60 + updateCameraItemData( + moduleStatusEntity, + cameraFaultList, + SensorCamera.DriversCameraSensing120 + )// 前右120 + updateCameraItemData( + moduleStatusEntity, + cameraFaultList, + SensorCamera.DriversCameraSensing120Left + )// 左120 + updateCameraItemData( + moduleStatusEntity, + cameraFaultList, + SensorCamera.DriversCameraSensing120Back + )// 后120 + updateCameraItemData( + moduleStatusEntity, + cameraFaultList, + SensorCamera.DriversCameraSensing120Right + )// 右120 + } + } + } + lastOpenedAdapter?.notifyItemChanged(index) + updateRedDot() + } + } + + private fun updateCameraItemData( + moduleStatusEntity: ModuleStatusEntity, + cameraFaultList: List, + sensorCamera: SensorCamera + ) { + val sensorStatusEntity = + moduleStatusEntity.sensorList.find { mt -> mt.sensorName == sensorCamera.title } + sensorStatusEntity?.let { + val cameraFault = + cameraFaultList.firstOrNull { cl -> cl.faultId.contains(sensorCamera.key) }//如果查找到 相关的则代表否有异常 + it.sensorIsOk = cameraFault == null + it.notOkColorRes = + cameraFault?.let { cf -> FaultLevel.getColor(cf.policyCode, cf.faultLevel) } ?: -1 + } + } + + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (!hidden) { + initData() + } + } + +// //更新FM 底盘相关传感器 +// private fun updateModuleVehicleControl() { +// //检测底盘 +// val fmEntity = fmDataMap?.get(FaultModuleId.VehicleControl) +// val sensorVehicleControl = ArrayList() +// if (fmEntity != null) { +// if (fmEntity.data != null) { +// val list = fmEntity.data!!.take(2) +// for (i in list.indices) { +// sensorVehicleControl.add(SensorStatusEntity(list[i].faultName, false)) +// } +// } +// } +// val moduleName = getString(R.string.module_chassis) +// val moduleStatusEntity = +// mModuleStatusEntity.find { mt -> +// mt.title == moduleName +// } +// if (moduleStatusEntity != null) { +// if (sensorVehicleControl.isEmpty()) { +// moduleStatusEntity.sensorList = sensorStatusNormalLog +// } else { +// moduleStatusEntity.sensorList = sensorVehicleControl +// } +// val overallStatus = moduleStatusEntity.sensorList.any { !it.sensorIsOk } +// moduleStatusEntity.sensorBg = +// if (overallStatus) R.drawable.rviz_fmd_icon_ipc else R.drawable.icon_ipc_error +// lastOpenedAdapter?.notifyItemChanged(mModuleStatusEntity.indexOfFirst { it.title == moduleName }) +// updateRedDot() +// } +// +// } +// +// //更新FM RTK相关传感器 +// private fun updateModuleRTK() { +// //检测底盘 +// val fmEntity = fmDataMap?.get(FaultModuleId.Localization) +// val sensorVehicleControl = ArrayList() +// if (fmEntity != null) { +// if (fmEntity.data != null) { +// for ((index, item) in fmEntity.data!!.withIndex()) { +// if (item.faultId.contains("MSFLOC")) { +// if (sensorVehicleControl.size > 2) { +// break +// } +// sensorVehicleControl.add(SensorStatusEntity(item.faultName, false)) +// } +// } +// +// } +// } +// val moduleName = getString(R.string.module_rtk) +// val moduleStatusEntity = +// mModuleStatusEntity.find { mt -> +// mt.title == moduleName +// } +// if (moduleStatusEntity != null) { +// if (sensorVehicleControl.isEmpty()) { +// moduleStatusEntity.sensorList = sensorStatusNormalLog +// } else { +// moduleStatusEntity.sensorList = sensorVehicleControl +// } +// val overallStatus = moduleStatusEntity.sensorList.any { !it.sensorIsOk } +// moduleStatusEntity.sensorBg = +// if (overallStatus) R.drawable.icon_ipc else R.drawable.icon_ipc_error +// lastOpenedAdapter?.notifyItemChanged(mModuleStatusEntity.indexOfFirst { it.title == moduleName }) +// updateRedDot() +// } +// +// } +// +// //更新FM 激光雷达相关传感器 +// private fun updateModuleLaserRadar() { +// //检测底盘 +// val fmEntity = fmDataMap?.get(FaultModuleId.HardwareDriver) +// val sensorVehicleControl = ArrayList() +// if (fmEntity != null) { +// if (fmEntity.data != null) { +// for ((index, item) in fmEntity.data!!.withIndex()) { +// if (item.faultId.contains("MSFLOC")) { +// if (sensorVehicleControl.size > 2) { +// break +// } +// sensorVehicleControl.add(SensorStatusEntity(item.faultName, false)) +// } +// } +// +// } +// } +// val moduleName = getString(R.string.module_laser_radar) +// val moduleStatusEntity = +// mModuleStatusEntity.find { mt -> +// mt.title == moduleName +// } +// if (moduleStatusEntity != null) { +// if (sensorVehicleControl.isEmpty()) { +// moduleStatusEntity.sensorList = sensorStatusNormalLog +// } else { +// moduleStatusEntity.sensorList = sensorVehicleControl +// } +// val overallStatus = moduleStatusEntity.sensorList.any { !it.sensorIsOk } +// moduleStatusEntity.sensorBg = +// if (overallStatus) R.drawable.rviz_fmd_icon_ipc else R.drawable.icon_ipc_error +// lastOpenedAdapter?.notifyItemChanged(mModuleStatusEntity.indexOfFirst { it.title == moduleName }) +// updateRedDot() +// } +// +// } +// +// //更新FM模块 包括 底盘、RTK、相机、激光雷达、毫米波雷达 +// @OptIn(DelicateCoroutinesApi::class) +// private fun updateModuleFM() { +// GlobalScope.launch(Dispatchers.IO) { +// if (fmDataMap != null) { +// for ((index, item) in fmDataMap!!.values.withIndex()) { +// if (item.data.isNullOrEmpty()) { +// if (item.tag == FaultModuleId.VehicleControl) {//底盘无异常 +// updateItem(getString(R.string.module_chassis), sensorStatusNormalLog) +// } else if (item.tag == FaultModuleId.Localization) {//RTK无异常 Localization下的MSFLOC +// updateItem(getString(R.string.module_rtk), sensorStatusNormalLog) +// } else if (item.tag == FaultModuleId.HardwareDriver) { +// //相机无异常 HardwareDriver下的Camera相关的 +// val moduleName = getString(R.string.module_camera) +// val moduleStatusEntity = +// mModuleStatusEntity.find { mt -> +// mt.title == moduleName +// } +// if (moduleStatusEntity != null && moduleStatusEntity.sensorList.isNotEmpty()) { +// moduleStatusEntity.sensorList.map { it.copy(sensorIsOk = true) } +// lastOpenedAdapter?.notifyItemChanged(mModuleStatusEntity.indexOfFirst { et -> et.title == moduleName }) +// updateRedDot() +// } +// //激光雷达无异常 HardwareDriver下的LidarDriver相关的 +// updateItem( +// getString(R.string.module_laser_radar), +// sensorStatusNormalLog +// ) +// } else if (item.tag == FaultModuleId.Perception) {//毫米波雷达无异常 Perception下的RadarFusion +// updateItem( +// getString(R.string.module_millimeter_wave_radar), +// sensorStatusNormalLog +// ) +// } +// } else { +// for ((i, t) in item.data!!.withIndex()) { +// if (t.faultId.lowercase(Locale.getDefault()) +// .contains(FaultModuleId.VehicleControl.name.lowercase(Locale.getDefault())) +// ) {//底盘故障 +// +// } else if (t.faultId.lowercase(Locale.getDefault()) +// .contains( +// (FaultModuleId.Localization.name + "_MSFLOC").lowercase( +// Locale.getDefault() +// ) +// ) +// ) {//RTK故障 +// +// } +// } +// } +// } +// } +// } +// +// } + + private fun updateItem( + moduleName: String, + sensorList: ArrayList, + isUpdateRedDot: Boolean = false + ) { + val moduleStatusEntity = + mModuleStatusEntity.find { mt -> + mt.title == moduleName + } + if (moduleStatusEntity != null) { + moduleStatusEntity.sensorList = sensorList + val overallStatus = moduleStatusEntity.sensorList.any { !it.sensorIsOk } + moduleStatusEntity.sensorBg = + if (overallStatus) R.drawable.rviz_fmd_icon_ipc else R.drawable.rviz_fmd_icon_ipc_error + lastOpenedAdapter?.notifyItemChanged(mModuleStatusEntity.indexOfFirst { it.title == moduleName }) + if (isUpdateRedDot) + updateRedDot() + } + } + + public fun setData(data: MutableMap?) { + if (fmDataMap == null) { + if (data != null) { + fmDataMap = data + } + } + } + + private fun initData() { + if (fmDataMap == null) { + val data = fmdAct.getFmEntityData() + if (data != null) { + fmDataMap = data + } + } + } + + private fun setText(textView: TextView, str: String) { + if (str.endsWith(getString(R.string.rviz_fmd_unknown)) || str.endsWith("未安装") || str == getString( + R.string.rviz_fmd_get_fail_hd_map_version + ) + ) { + val index = str.indexOf(':'); + if (index != -1) { + val spannableString = SpannableString(str) + spannableString.setSpan( + spanError, + index + 1, + str.length, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + textView.text = spannableString + return + } + } + textView.text = str + } + + //更新域控模块状态 + private fun updateModuleHost(sensorName: String, sensorIsOk: Boolean) { + val name = Utils.getIPLastSegment(sensorName) + val moduleName = getString(R.string.rviz_fmd_module_host) + val moduleStatusEntity = + mModuleStatusEntity.find { mt -> + mt.title == moduleName + } + if (moduleStatusEntity != null) { + val sensorStatus = moduleStatusEntity.sensorList.find { st -> st.sensorName == name } + if (sensorStatus == null) { + moduleStatusEntity.sensorList.add(SensorStatusEntity(name, sensorIsOk)) + if (moduleStatusEntity.sensorList.isNotEmpty()) { + moduleStatusEntity.sensorList.sortWith { s1, s2 -> + s1.sensorName.compareTo(s2.sensorName) + } + + } + } else { + sensorStatus.sensorIsOk = sensorIsOk + } + val overallStatus = moduleStatusEntity.sensorList.any { !it.sensorIsOk } + moduleStatusEntity.sensorBg = + if (overallStatus) R.drawable.rviz_fmd_icon_ipc else R.drawable.rviz_fmd_icon_ipc_error + lastOpenedAdapter?.notifyItemChanged(mModuleStatusEntity.indexOfFirst { it.title == moduleName }) + updateRedDot() + } + } + + + //更新红点 + private fun updateRedDot() { + val countOfFailedSensors = mModuleStatusEntity.sumOf { module -> + module.sensorList.count { sensor -> + !sensor.sensorIsOk + } + } + fmdAct.updateRedDot(TAG, countOfFailedSensors) + } + + /******************SSH 连接状态*********************/ + override fun onSshConnecting( + host: SSHHostBean, + rosHostArgumentPosition: Int, + isInserted: Boolean + ) { + updateModuleHost(host.hostname, false) + } + + override fun onSshConnected(ssh: SSH) { + updateModuleHost(ssh.host.hostname, true) + } + + override fun onSshDisconnected(host: SSHHostBean) { + updateModuleHost(host.hostname, false) + } + + override fun onSshConnectFailure(host: SSHHostBean, msg: String) { + updateModuleHost(host.hostname, false) + } + + override fun onAutopilotAbility( + isAutopilotAbility: Boolean, + launchConditionData: LaunchConditionData?, + unableAutopilotReasons: ArrayList? + ) { + updateAutopilotAbility() + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/resource/SystemResourceFrag.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/resource/SystemResourceFrag.kt new file mode 100644 index 0000000000..8209f64ea4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/fragments/resource/SystemResourceFrag.kt @@ -0,0 +1,245 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.resource + +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.mogo.eagle.core.utilcode.util.ToastUtils +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.Utils +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey +import com.zhjt.mogo_core_function_devatools.rviz.dialog.DockersDialog +import com.zhjt.mogo_core_function_devatools.rviz.dialog.InputUserPwdDialog +import com.zhjt.mogo_core_function_devatools.rviz.dialog.ShowConfigDialog +import com.zhjt.mogo_core_function_devatools.rviz.dialog.StartupConfigDialog +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DiskInfo +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerBean +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.DockerStatus +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.RosHostArgument +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.StartupConfig +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH +import com.zhjt.mogo_core_function_devatools.rviz.ssh.constant.MogoCommand +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.DockerCommandHandler +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean +import com.zhjt.mogo_core_function_devatools.rviz.ui.fragments.FmdBaseFragment +import com.zhjt.mogo_core_function_devatools.rviz.widgets.ros.host.OnRosHostClickListener +import com.zhjt.mogo_core_function_devatools.rviz.widgets.ros.host.RosHostView +import com.zhjt.mogo_core_function_devatools.rviz.widgets.ros.host.RosHostView.OnRosHostViewListener + +/** + * 系统资源 UI及业务逻辑 + */ +class SystemResourceFrag : FmdBaseFragment(), OnRosHostClickListener, + OnRosHostViewListener { + private var isInit = false + private lateinit var rosHostsView: RosHostView + + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (!hidden) { + if (!isInit) { + isInit = true + rosHostsView.setDatas( + fmdAct.getRosHostArguments(), + fmdAct.getCloudMapVersion() + ) + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + rosHostsView = view.findViewById(R.id.ros_hosts_view) + rosHostsView.setOnRosHostClickListener(this) + rosHostsView.addListener(this) + initFlowBusEvent() + } + + override fun onDestroyView() { + super.onDestroyView() + rosHostsView.removeListener() + } + + override fun getContentViewResource(inflater: LayoutInflater, container: ViewGroup?): View { + return inflater.inflate(R.layout.rviz_fmd_frag_system_resource, container, false) + } + + //更新红点 + private fun updateRedDot() { + val list = fmdAct.getRosHostArguments() + val cloudMapVersion = fmdAct.getCloudMapVersion() + var count: Int = 0 + if (!list.isNullOrEmpty()) { + for (it in list) { + if (it.isConnectFailure) { + count++ + } else { + if (it.cpuUsageRate >= 90) { + count++ + } + if (it.diskUsageRate.first >= 90) { + count++ + } + if (it.memUsageRate.first >= 90) { + count++ + } + if (it.swapUsageRate.first >= 90) { + count++ + } + val localMapVersion: String = it.dockerVersion + if (TextUtils.isEmpty(cloudMapVersion) || + TextUtils.equals(cloudMapVersion, "未知") || + TextUtils.equals( + cloudMapVersion, + getString(R.string.rviz_fmd_cloud_map_version_unknown) + ) + ) { + if (TextUtils.isEmpty(localMapVersion) || + TextUtils.equals(localMapVersion, "未知") + ) { + count++ + } + } else { + if (!TextUtils.equals(localMapVersion, cloudMapVersion)) { + count++ + } + } + } + } + } + + fmdAct.updateRedDot(TAG, count) + } + + private fun initFlowBusEvent() { + FlowBus.with(EventKey.QUERY_DOCKER_PS) + .register(this) { + if (it.dockers.isEmpty()) { + ToastUtils.showLong("Docker列表查询失败") + } else { + DockersDialog(fmdAct, it).show() + } + fmdAct.dismissLoadingDialog() + } + FlowBus.with(EventKey.QUERY_DISK_STATUS) + .register(this) { + if (TextUtils.isEmpty(it.data)) { + ToastUtils.showLong("磁盘信息查询失败") + } else { + ShowConfigDialog( + fmdAct, + Utils.getIPLastSegment(it.host.hostname) + "--磁盘概览", + it.data + ).show() + } + fmdAct.dismissLoadingDialog() + } + FlowBus.with(EventKey.DOCKER_STATUS) + .register(this) { + if (it.status == DockerCommandHandler.CONNECT_STATUS.CONNECTED) { + val ssh = fmdAct.getSSH(it.host) + if (ssh != null) { + queryStartupConfig(ssh) + } + } else { + when (it.status) { + DockerCommandHandler.CONNECT_STATUS.FAILED -> { + ToastUtils.showLong("无法与主机“" + it.host.hostname + "”中的Docker建立连接") + } + + DockerCommandHandler.CONNECT_STATUS.CLOSE -> { + ToastUtils.showLong("已断开主机“" + it.host.hostname + "”中的Docker连接") + } + + DockerCommandHandler.CONNECT_STATUS.CLOSE_DELAYED -> { + ToastUtils.showLong("已自动断开主机“" + it.host.hostname + "”中的Docker连接") + } + } + fmdAct.dismissLoadingDialog() + } + } + FlowBus.with(EventKey.QUERY_STARTUP_CONFIG) + .register(this) { + if (it.configs == null || it.configs.isEmpty()) { + ToastUtils.showLong("启动配置查询失败") + } else { + StartupConfigDialog( + fmdAct, + Utils.getIPLastSegment(it.host.hostname) + "--StartupConfigCache.json", + it, + object : StartupConfigDialog.OnStartupConfigListener { + override fun onQuery(host: SSHHostBean, path: String) { + fmdAct.execDockerCmd( + host, + MogoCommand.QUERY_DOCKER_CONFIG_CONTENT + path + ) + } + + override fun onDismiss(host: SSHHostBean) { + val ssh = fmdAct.getSSH(host) + if (ssh != null) { + //延迟几分钟后自动关闭Docker连接 + ssh.startDockerDisconnectTimer(true) + //直接关闭Docker连接 +// bridge.disconnectDocker(bridge) + } + } + }).show() + } + fmdAct.dismissLoadingDialog() + } + } + + override fun onDockerClick(hostBean: SSHHostBean) { + fmdAct.execCmd(hostBean, MogoCommand.QUERY_DOCKER_PS_A, "正在查询Docker信息") + } + + override fun onConfigClick(hostBean: SSHHostBean) { + val ssh = fmdAct.getSSH(hostBean) + if (ssh != null) { + ssh.stopDockerDisconnectTimer();//停止自动断开Docker连接定时器 + if (ssh.isDockerOpened) { + queryStartupConfig(ssh) + } else { + fmdAct.connectDocker(ssh) + } + } + } + + override fun onDiskClick(hostBean: SSHHostBean) { + fmdAct.execCmd(hostBean, MogoCommand.QUERY_DF_H, "正在查询磁盘信息") + } + + override fun onReconnect(argument: RosHostArgument) { + if (TextUtils.equals( + "用户名或密码错误,请重试", + argument.connectFailureReason + ) + ) { + //密码错误 + InputUserPwdDialog( + fmdAct, + argument.host, + object : InputUserPwdDialog.OnClickListener { + override fun onConfirm() { + argument.resetConnectFailureCode() + fmdAct.openConnection(argument.host) + } + }).show() + } else { + argument.resetConnectFailureCode() + fmdAct.openConnection(argument.host) + } + + } + + private fun queryStartupConfig(ssh: SSH) { + fmdAct.execCmd(ssh, MogoCommand.QUERY_STARTUP_CONFIG, "正在查询启动配置") + } + + override fun onUpdateNotify() { + updateRedDot() + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindow.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindow.java new file mode 100644 index 0000000000..c3a4e86560 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindow.java @@ -0,0 +1,119 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.views; + +import android.content.Context; +import android.graphics.Rect; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.constant.FaultLevel; + + +/** + * 2017/1/10. + * Description:全局悬浮窗口 + */ + +public class ColorHintFloatWindow extends LinearLayout { + + + private final WindowManager wm; + //此wmParams变量为获取的全局变量,用以保存悬浮窗口的属性 + private WindowManager.LayoutParams wmParams; + + private float mInViewX; + private float mInViewY; + private View sshConnectErrorView = null; + + public void setWmParams(WindowManager.LayoutParams wmParams) { + this.wmParams = wmParams; + } + + public ColorHintFloatWindow(@NonNull Context context, boolean isOverviewFrag) { + super(context, null, 0); + setOrientation(LinearLayout.VERTICAL); + setBackgroundResource(R.drawable.rvzi_fmd_bg_color_hint_float); + setPadding(10, 10, 10, 10); + wm = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + //加载布局 + init(isOverviewFrag); + } + + private void init(boolean isOverviewFrag) { + // 遍历布局资源 ID 数组 + for (FaultLevel level : FaultLevel.getALL()) { + addChild(level); + } + if (isOverviewFrag) { + sshConnectErrorView = addChild(FaultLevel.SSH_CONNECT_ERROR); + } + } + + private View addChild(FaultLevel level) { + View view = View.inflate(getContext(), R.layout.rviz_fmd_item_color_hint, null); + TextView name = view.findViewById(R.id.name); + View view1 = view.findViewById(R.id.view); + name.setText(level.getMsg() + ":"); + view1.setBackgroundColor(getContext().getColor(level.getColorRes())); + this.addView(view); + return view; + } + + public void updateSshConnectErrorView(boolean isOverviewFrag) { + if (isOverviewFrag) { + if (sshConnectErrorView == null) { + sshConnectErrorView = addChild(FaultLevel.SSH_CONNECT_ERROR); + } + } else { + if (sshConnectErrorView != null) { + removeView(sshConnectErrorView); + sshConnectErrorView = null; + } + } + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + //获取到状态栏的高度 + Rect frame = new Rect(); + getWindowVisibleDisplayFrame(frame); + int statusBarHeight = frame.top; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // 获取相对View的坐标,即以此View左上角为原点 + mInViewX = event.getX(); + mInViewY = event.getY(); + // 获取相对屏幕的坐标,即以屏幕左上角为原点 + float mDownInScreenX = event.getRawX(); + float mDownInScreenY = event.getRawY() - statusBarHeight; + float mInScreenX = mDownInScreenX; + float mInScreenY = mDownInScreenY; + break; + case MotionEvent.ACTION_MOVE: + // 更新浮动窗口位置参数 + mInScreenX = event.getRawX(); + mInScreenY = event.getRawY() - statusBarHeight; + wmParams.x = (int) (mInScreenX - mInViewX); + wmParams.y = (int) (mInScreenY - mInViewY); + updateViewLayout(); + break; + + case MotionEvent.ACTION_UP: + ColorHintFloatWindowManager.setFloatWindowLocation(wmParams.x, wmParams.y); + break; + } + return true; + } + + public void updateViewLayout() { + wm.updateViewLayout(this, wmParams); + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindowManager.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindowManager.java new file mode 100644 index 0000000000..1b97e6b15d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/ColorHintFloatWindowManager.java @@ -0,0 +1,95 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.views; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.ViewTreeObserver; +import android.view.WindowManager; + +import com.zhidao.support.adas.high.common.MMKVUtils; + + +public class ColorHintFloatWindowManager { + private final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + private ColorHintFloatWindow floatWindow = null; + private WindowManager wm = null; + + public ColorHintFloatWindowManager() { + } + + + public void remove() { + if (floatWindow != null) { + wm.removeView(floatWindow); + floatWindow = null; + wm = null; + } + } + + public void show(Context context, boolean isOverviewFrag) { + if (floatWindow == null) { + wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + //检查版本,注意当type为TYPE_APPLICATION_OVERLAY时,铺满活动窗口,但在关键的系统窗口下面,如状态栏或IME + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; + } + layoutParams.format = PixelFormat.RGBA_8888; + layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + layoutParams.gravity = Gravity.START | Gravity.TOP; + layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + floatWindow = new ColorHintFloatWindow(context, isOverviewFrag); + floatWindow.setWmParams(layoutParams); + int x = getFloatWindowLocationX(); + if (x == -1) { + layoutParams.x = 10000; + layoutParams.y = 0; + floatWindow.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int floatingWidth = floatWindow.getWidth(); + if (floatingWidth != 0) { + // 移除监听器以避免重复调用 + floatWindow.getViewTreeObserver().removeOnGlobalLayoutListener(this); + DisplayMetrics metrics2 = context.getResources().getDisplayMetrics(); + int screenWidth = metrics2.widthPixels; + layoutParams.x = screenWidth - floatingWidth; + layoutParams.y = getFloatWindowLocationY(); + floatWindow.updateViewLayout(); + setFloatWindowLocation(layoutParams.x, layoutParams.y); + } + } + }); + } else { + layoutParams.x = getFloatWindowLocationX(); + layoutParams.y = getFloatWindowLocationY(); + } + wm.addView(floatWindow, layoutParams); + } + } + + public void updateSshConnectErrorView(boolean isOverviewFrag) { + if (floatWindow != null) { + floatWindow.updateSshConnectErrorView(isOverviewFrag); + } + } + + + public static void setFloatWindowLocation(int x, int y) { + MMKVUtils.getInstance().put("color_hint_float_window_x", x); + MMKVUtils.getInstance().put("color_hint_float_window_y", y); + } + + private int getFloatWindowLocationX() { + return MMKVUtils.getInstance().getInt("color_hint_float_window_x", -1); + } + + private int getFloatWindowLocationY() { + return MMKVUtils.getInstance().getInt("color_hint_float_window_y"); + } + +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/SensorStatusView.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/SensorStatusView.kt new file mode 100644 index 0000000000..9fd88beed9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/SensorStatusView.kt @@ -0,0 +1,206 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.views + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.StateListDrawable +import android.graphics.drawable.VectorDrawable +import android.text.TextUtils +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.widget.CheckBox +import android.widget.GridLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import com.zhjt.mogo_core_function_devatools.rviz.R +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.ModuleStatusEntity +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.SensorStatusEntity + + +/** + * 传感器状态View + */ +class SensorStatusView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + private lateinit var tvModuleTitle: TextView + private lateinit var ivBg: ImageView + private lateinit var cl: ConstraintLayout + private lateinit var glSensorStatus: GridLayout + + init { + initView() + } + + private fun initView() { + LayoutInflater.from(context) + .inflate(R.layout.rviz_fmd_item_car_status_module_sensor_status, this, true) + tvModuleTitle = findViewById(R.id.tvModuleTitle); + ivBg = findViewById(R.id.ivBg); + cl = findViewById(R.id.cl); + glSensorStatus = findViewById(R.id.glSensorStatus); + // TODO 模拟数据 +// val sensorStatus = ArrayList() +// sensorStatus.add(SensorStatusEntity("102", true)) +// sensorStatus.add(SensorStatusEntity("103", true)) +// sensorStatus.add(SensorStatusEntity("104", false)) +// sensorStatus.add(SensorStatusEntity("105", true)) +// sensorStatus.add(SensorStatusEntity("106", true)) +// sensorStatus.add(SensorStatusEntity("107", true)) +// +// val carSensorStatusEntity = +// ModuleStatusEntity("域控主机", R.drawable.icon_camera, sensorStatus, false) +// +// updateSensors(carSensorStatusEntity) + + } + + + /** + * 更新模块状态 + */ + fun updateSensors(moduleStatusEntity: ModuleStatusEntity) { + setModuleTitle(moduleStatusEntity.title) + changeBg(moduleStatusEntity.sensorBg) + updateSensors(moduleStatusEntity.isLog, moduleStatusEntity.sensorList) + } + + /** + * 设置标题 + */ + private fun setModuleTitle(title: String) { + tvModuleTitle.text = title + } + + /** + * 修改背景图标 + */ + private fun changeBg(backgroundResource: Int) { + ivBg.setImageDrawable(resources.getDrawable(backgroundResource)) + } + + /** + * 修改是否正常状态 + */ + private fun changeStatus(isOk: Boolean) { + if (isOk) { + cl.setBackgroundResource(R.drawable.rviz_fmd_bg_item_normal) + ivBg.setColorFilter(Color.BLUE) + } else { + cl.setBackgroundResource(R.drawable.rviz_fmd_bg_item_error) + ivBg.setColorFilter(Color.RED) + } + } + + /** + * 更新内部的子传感器状态 + */ + private fun updateSensors(isLog: Boolean = false, sensorStatus: ArrayList) { + glSensorStatus.removeAllViews() + var moduleIsOk = true + sensorStatus.forEachIndexed { index, item -> + // 日志数据最多展示3条 + if (isLog && index > 3) { + return + } + // 网格最多展示6个数据 + if (!isLog && index > 6) { + return + } + // 只要有一个异常的信息就设置模块为异常 + if (!item.sensorIsOk) { + moduleIsOk = false + } + + // 在设置布局参数时指定layout_columnWeight + val layoutParams = GridLayout.LayoutParams() + + if (isLog) { + layoutParams.columnSpec = + GridLayout.spec(0, 1f) // 权重1f + } else { + layoutParams.columnSpec = + GridLayout.spec(GridLayout.UNDEFINED, 1f) // 权重1f + layoutParams.rowSpec = + GridLayout.spec(GridLayout.UNDEFINED, 1f) // 权重1f + } + + layoutParams.height = LayoutParams.WRAP_CONTENT + layoutParams.width = LayoutParams.WRAP_CONTENT + + val sensorItemView = CheckBox(context) + sensorItemView.isEnabled = false + sensorItemView.layoutParams = layoutParams + val buttonDrawable: Drawable? + val textColor: ColorStateList? + if (item.notOkColorRes == -1) { + buttonDrawable = + ContextCompat.getDrawable(context, R.drawable.rviz_fmd_selector_status_icon) + textColor = ContextCompat.getColorStateList(context, R.color.rviz_fmd_selector_txt_color) + } else { + val color = ContextCompat.getColor(context, item.notOkColorRes) + textColor = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf() // 默认状态 + ), + intArrayOf( + ContextCompat.getColor(context, R.color.rvizFmdColorBlock), + color + ) + ) + + buttonDrawable = StateListDrawable() + val vectorDrawableSelected = + ContextCompat.getDrawable( + context, + R.drawable.rviz_fmd_icon_normal + ) as VectorDrawable + val vectorDrawableDefault = + ContextCompat.getDrawable(context, R.drawable.rviz_fmd_icon_error) as VectorDrawable + vectorDrawableDefault.setTint(color) + buttonDrawable.addState( + intArrayOf(android.R.attr.state_selected), + vectorDrawableSelected + ) + buttonDrawable.addState( + intArrayOf(android.R.attr.state_checked), + vectorDrawableSelected + ) + buttonDrawable.addState(intArrayOf(), vectorDrawableDefault) // 默认状态 + } + buttonDrawable?.let { + sensorItemView.buttonDrawable = it + } + textColor?.let { + sensorItemView.setTextColor(it) + } + sensorItemView.maxLines = 1 + sensorItemView.ellipsize = TextUtils.TruncateAt.END + sensorItemView.textSize = 24f + sensorItemView.setPadding( + 10, + 10, + 10, + 10 + ) + sensorItemView.compoundDrawablePadding = 5 + sensorItemView.gravity = Gravity.CENTER_VERTICAL + + // 更新数据 + sensorItemView.text = item.sensorName + sensorItemView.isChecked = item.sensorIsOk + + glSensorStatus.addView(sensorItemView) + } + + changeStatus(moduleIsOk) + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/StateBarView.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/StateBarView.java new file mode 100644 index 0000000000..69caeff9fe --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/ui/views/StateBarView.java @@ -0,0 +1,248 @@ +package com.zhjt.mogo_core_function_devatools.rviz.ui.views; + +import static kotlinx.coroutines.CoroutineScopeKt.MainScope; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Build; +import android.text.Html; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; + +import com.mogo.eagle.core.utilcode.util.NetworkUtils; +import com.zhjt.mogo.adas.data.AdasConstants; +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.DetectHtml; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.NetworkUtilsExtend; +import com.zhjt.mogo_core_function_devatools.rviz.constant.AppConfigInfo; +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.AdasConnectionStatus; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.CoroutineScopeKt; +import mogo.telematics.pad.MessagePad; + +/** + * 连接以及状态展示 + */ +public class StateBarView extends LinearLayout implements LifecycleOwner, NetworkUtilsExtend.NetworkCallbackListener { + private static final String TAG = StateBarView.class.getSimpleName(); + private final CoroutineScope scopeSubscriber = CoroutineScopeKt.CoroutineScope(MainScope().getCoroutineContext()); + private final LifecycleRegistry lifecycle = new LifecycleRegistry(this); + private TextView netNameView; + private TextView ipView; + private TextView vehicleNumberView; + private TextView adasStateView; + + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + + @Override + public void onConnected(@Nullable Network network) { + setHintTextView(netNameView, getNetWorkType()); + setHintTextView(ipView, getIpAddressString()); + } + + @Override + public void onDisconnected() { + setHintTextView(netNameView, getNetWorkType()); + setHintTextView(ipView, getIpAddressString()); + } + + @Override + public void onLinkChanged(@Nullable Network network, @Nullable LinkProperties linkProperties) { + + } + + + public StateBarView(@NonNull Context context) { + super(context); + init(context); + } + + public StateBarView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public StateBarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + lifecycle.setCurrentState(Lifecycle.State.CREATED); + LayoutInflater.from(context).inflate(R.layout.rviz_fmd_view_state_bar, this, true); + setListener(); + initView(); + getNetWorkType(); +// FlowBus.INSTANCE.with(EventKey.GET_VEHICLE_CONFIG).post(scopeSubscriber, EventKey.GET_VEHICLE_CONFIG); + FlowBus.INSTANCE.with(EventKey.UPDATE_CAR_CONFIG_STATE).register(this, it -> { + setHintTextView(vehicleNumberView, it.getPlateNumber()); + }); + FlowBus.INSTANCE.with(EventKey.UPDATE_ADAS_CONNECT_STATE).register(this, it -> { + String msg = getResources().getString(R.string.rviz_fmd_disconnected); + AdasConstants.IpcConnectionStatus ipcConnectionStatus = it.getIpcConnectionStatus(); + String reason = it.getReason(); + if (ipcConnectionStatus == AdasConstants.IpcConnectionStatus.CONNECTED) { + msg = getResources().getString(R.string.rviz_fmd_connected); + } else if (ipcConnectionStatus == AdasConstants.IpcConnectionStatus.DISCONNECTED) { + msg = "连接失败"; + } else if (ipcConnectionStatus == AdasConstants.IpcConnectionStatus.CONNECTING) { + msg = "连接中"; + if (!TextUtils.isEmpty(reason)) { + if (reason.contains("(")) { + String[] str = reason.split("("); + msg = str[0]; + } else { + msg = reason; + } + } + } + setHintTextView(adasStateView, msg); + if (ipcConnectionStatus != AdasConstants.IpcConnectionStatus.CONNECTED) { + setHintTextView(vehicleNumberView, getResources().getString(R.string.rviz_fmd_disconnected)); + } + }); + + } + + + private void setListener() { + netNameView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent wifiSettingsIntent = new Intent("android.settings.WIFI_SETTINGS"); + getContext().startActivity(wifiSettingsIntent); + } + }); + } + + + //初始化 + private void initView() { + netNameView = findViewById(R.id.net_name_view); + ipView = findViewById(R.id.ip_view); + vehicleNumberView = findViewById(R.id.vehicle_number_view); + adasStateView = findViewById(R.id.adas_state_view); + setHintTextView(netNameView, getNetWorkType()); + setHintTextView(ipView, getIpAddressString()); + setHintTextView(vehicleNumberView, TextUtils.isEmpty(AppConfigInfo.INSTANCE.getPlateNumber()) ? getResources().getString(R.string.rviz_fmd_disconnected) : AppConfigInfo.INSTANCE.getPlateNumber()); + setHintTextView(adasStateView, getResources().getString(R.string.rviz_fmd_disconnected)); + } + + /** + * 获取当前IP + * + * @return IP + */ + private String getIpAddressString() { + try { + for (Enumeration enNetI = NetworkInterface.getNetworkInterfaces(); enNetI.hasMoreElements(); ) { + NetworkInterface netI = enNetI.nextElement(); + for (Enumeration enumIpAddr = netI.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (inetAddress instanceof Inet4Address && !inetAddress.isLoopbackAddress()) { + return inetAddress.getHostAddress(); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return getResources().getString(R.string.rviz_fmd_unknown); + } + + /** + * 获取网络状态 + * + * @return 网络类型 + */ + private String getNetWorkType() { + String networkType = getResources().getString(R.string.rviz_fmd_disconnected); + ConnectivityManager connectivityManager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager != null) { + Network networks = connectivityManager.getActiveNetwork(); + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(networks); + if (networkCapabilities != null) { + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + String ssid = NetworkUtils.getSSID(); + if (!TextUtils.isEmpty(ssid) && !ssid.contains("unknown ssid")) { + networkType = ssid + getResources().getString(R.string.rviz_fmd_wifi); + } else { + networkType = "WiFi"; + } + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + networkType = getResources().getString(R.string.rviz_fmd_mobile_network); + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + networkType = getResources().getString(R.string.rviz_fmd_ethernet); + } else { + networkType = getResources().getString(R.string.rviz_fmd_other_network); + } + } + } + return networkType; + } + + //设置6个提示信息View数据 + private void setHintTextView(TextView textView, String text) { + if (DetectHtml.isHtml(text)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + textView.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)); + } else { + textView.setText(Html.fromHtml(text)); + } + } else { + if (getResources().getString(R.string.rviz_fmd_disconnected).equals(text) || getResources().getString(R.string.rviz_fmd_unknown).equals(text) || "连接失败".equals(text)) { + textView.setTextColor(getResources().getColor(R.color.rviz_fmd_status_error)); + } else if (getResources().getString(R.string.rviz_fmd_gain).equals(text) || "连接中".equals(text) || "重连中".equals(text)) { + textView.setTextColor(Color.YELLOW); + } else { + textView.setTextColor(getResources().getColor(R.color.rviz_fmd_status_normal)); + } + textView.setText(text); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + lifecycle.setCurrentState(Lifecycle.State.STARTED); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + lifecycle.setCurrentState(Lifecycle.State.DESTROYED); + NetworkUtilsExtend.Companion.removeNetworkCallback(this); + } + + public View getVehicleNumberView() { + return vehicleNumberView; + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/CustomCheckBox.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/CustomCheckBox.java new file mode 100644 index 0000000000..99080d3268 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/CustomCheckBox.java @@ -0,0 +1,23 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets; + + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatCheckBox; + +public class CustomCheckBox extends AppCompatCheckBox { + public CustomCheckBox(@NonNull Context context) { + super(context); + } + + public CustomCheckBox(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CustomCheckBox(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/FmdProgressBar.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/FmdProgressBar.java new file mode 100644 index 0000000000..2806d7599a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/FmdProgressBar.java @@ -0,0 +1,158 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.zhjt.mogo_core_function_devatools.rviz.R; + +import java.text.DecimalFormat; +import java.util.Locale; + +public class FmdProgressBar extends ConstraintLayout { + private boolean progressWarning = true; + private int progressWarningValue = 90; + private int progressMax; + private final DecimalFormat decimalFormat = new DecimalFormat("#.##"); // 保留两位小数 + private TextView totalText; + private TextView progressPercentText; + private ProgressBar progressBar; + + public FmdProgressBar(Context context) { + super(context); + } + + public FmdProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, -1, 0); + } + + public FmdProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + public FmdProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + private void init(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { + LayoutInflater.from(context).inflate(R.layout.rviz_fmd_view_progress_bar, this, true); + TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.RvizFmdProgressBar, defStyleAttr, defStyleRes); + progressWarning = a.getBoolean(R.styleable.RvizFmdProgressBar_progress_warning, true); + progressWarningValue = a.getInt(R.styleable.RvizFmdProgressBar_progress_warning_value, 90); + progressMax = a.getInt(R.styleable.RvizFmdProgressBar_progress_max, 100); + int progressTextColor = a.getColor(R.styleable.RvizFmdProgressBar_progress_text_color, 0xFF000000); + float progressTextSize = a.getDimensionPixelSize(R.styleable.RvizFmdProgressBar_progress_text_size, -1); + a.recycle(); + totalText = findViewById(R.id.total_text); + progressPercentText = findViewById(R.id.progress_percent_text); + progressBar = findViewById(R.id.progressBar); + setProgressWarning(progressWarning); + setProgressMax(progressMax); + setTextColor(progressTextColor); + if (progressTextSize != -1) { + totalText.setTextSize(TypedValue.COMPLEX_UNIT_PX, progressTextSize); + progressPercentText.setTextSize(TypedValue.COMPLEX_UNIT_PX, progressTextSize); + } + } + + public void setProgressMax(int max) { + progressBar.setMax(max); + } + + /** + * 设置字体颜色 + * + * @param value 颜色值 + */ + public void setTextColor(@ColorInt int value) { + totalText.setTextColor(value); + progressPercentText.setTextColor(value); + } + + /** + * 设置字体大小 + * + * @param size sp + */ + public void setTextSize(float size) { + totalText.setTextSize(size); + progressPercentText.setTextSize(size); + } + + /** + * 是否启用预警 + * + * @param progressWarning 是否启用 + */ + public void setProgressWarning(boolean progressWarning) { + this.progressWarning = progressWarning; + progressBar.setSecondaryProgress(0); + } + + /** + * 预警值 progressWarning true 时生效 + * + * @param progressWarningValue 值 + */ + public void setProgressWarningValue(int progressWarningValue) { + this.progressWarningValue = progressWarningValue; + } + + /** + * 设置当前和总计提示 + * + * @param total 总计 + */ + public void setTotalText(CharSequence total) { + totalText.setText(total); + } + + public void setProgress(double progress) { + if (progress < 0.0) { + progress = 0.0; + progressPercentText.setText(""); + } else { + if (progress > progressMax) { + progress = progressMax; + } + String format = decimalFormat.format(progress); + progressPercentText.setText(String.format(Locale.getDefault(), "%s%%", format)); + } + progress((int) Math.round(progress)); + } + + public void setProgress(int progress) { + if (progress < 0) { + progress = 0; + progressPercentText.setText(""); + } else { + progressPercentText.setText(String.format(Locale.getDefault(), "%d%%", progress)); + } + progress(progress); + } + + private void progress(int progress) { + if (progressWarning) { + if (progress >= progressWarningValue) { + progressBar.setSecondaryProgress(progress); + } else { + if (progressBar.getSecondaryProgress() != 0) { + progressBar.setSecondaryProgress(0); + } + progressBar.setProgress(progress); + } + } else { + progressBar.setProgress(progress); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/JustifiedTextView.kt b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/JustifiedTextView.kt new file mode 100644 index 0000000000..73dac213da --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/JustifiedTextView.kt @@ -0,0 +1,47 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView + +class JustifiedTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatTextView(context, attrs, defStyleAttr) { + + override fun onDraw(canvas: Canvas) { + val text = text.toString() + if (text.isEmpty()) return + + val paint = paint + val totalWidth = width.toFloat() + val charWidth = paint.measureText(text) / text.length + + // 计算每个字符之间的间距 + val space = if (text.length > 1) { + (totalWidth - charWidth * text.length) / (text.length - 1) + } else { + 0f + } + + var x = 0f + + // 将文本居中对齐 + canvas.save() + canvas.translate( + (totalWidth - (charWidth * text.length + space * (text.length - 1))) / 2, + 0f + ) + + // 绘制每个字符,均匀分布并两端对齐 + for (i in text.indices) { + val char = text[i].toString() + canvas.drawText(char, x, baseline.toFloat(), paint) + x += charWidth + space + } + + canvas.restore() + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/MyLinearLayoutManager.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/MyLinearLayoutManager.java new file mode 100644 index 0000000000..320cf9640d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/MyLinearLayoutManager.java @@ -0,0 +1,37 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + + +public class MyLinearLayoutManager extends LinearLayoutManager { + public MyLinearLayoutManager(Context context) { + super(context); + } + + public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + } + + public MyLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return false; + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + //override this method and implement code as below + try { + super.onLayoutChildren(recycler, state); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/OnRosHostClickListener.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/OnRosHostClickListener.java new file mode 100644 index 0000000000..cfe1d09637 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/OnRosHostClickListener.java @@ -0,0 +1,16 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets.ros.host; + +import androidx.annotation.NonNull; + +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.RosHostArgument; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +public interface OnRosHostClickListener { + void onDockerClick(@NonNull SSHHostBean hostBean); + + void onConfigClick(@NonNull SSHHostBean hostBean); + + void onDiskClick(@NonNull SSHHostBean hostBean); + + void onReconnect(@NonNull RosHostArgument argument); +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostAdapter.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostAdapter.java new file mode 100644 index 0000000000..87f0b8e417 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostAdapter.java @@ -0,0 +1,222 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets.ros.host; + +import android.graphics.Color; +import android.os.Build; +import android.text.Html; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.mogo.eagle.core.utilcode.util.SpanUtils; +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseAdapter; +import com.zhjt.mogo_core_function_devatools.rviz.common.base.BaseViewHolder; +import com.zhjt.mogo_core_function_devatools.rviz.common.utils.DetectHtml; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.RosHostArgument; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; +import com.zhjt.mogo_core_function_devatools.rviz.widgets.FmdProgressBar; + +import java.util.Locale; + +import kotlin.Pair; + +public class RosHostAdapter extends BaseAdapter { + private String cloudMapVersion = "未知"; + private OnRosHostClickListener listener; + + public void setCloudMapVersion(String cloudMapVersion) { + this.cloudMapVersion = cloudMapVersion; + } + + public void notifyConnectFailure(SSHHostBean host) { + if (mDatas != null) { + RosHostArgument tem = new RosHostArgument(host); + int index = mDatas.indexOf(tem); + if (index > -1) { + notifyItemChanged(index); + } + } + } + + public void setOnItemClickListener(OnRosHostClickListener l) { + this.listener = l; + } + + @Override + protected View getItemViewResource(ViewGroup viewGroup) { + return LayoutInflater.from(mContext).inflate(R.layout.rviz_fmd_item_ros, viewGroup, false); + } + + @Override + protected MyViewHolder getViewHolder(View view) { + return new MyViewHolder(view, this); + } + + + @Override + protected void onBindDataToItem(MyViewHolder viewHolder, RosHostArgument data, int position) { + viewHolder.ipView.setText(data.host.getHostname()); + viewHolder.rosMasterView.setVisibility(data.isRosMaster() ? View.VISIBLE : View.GONE); + if (data.isConnectFailure) { + viewHolder.btnDocker.setVisibility(View.GONE); + viewHolder.btnConfig.setVisibility(View.GONE); + viewHolder.btnDisk.setVisibility(View.GONE); + viewHolder.layoutData.setVisibility(View.GONE); + viewHolder.loading.setVisibility(View.GONE); + viewHolder.viewFailure.setText(data.connectFailureReason); + viewHolder.viewFailure.setSelected(true); + viewHolder.layoutReconnect.setVisibility(View.VISIBLE); + } else { + viewHolder.layoutReconnect.setVisibility(View.GONE); + viewHolder.layoutData.setVisibility(View.VISIBLE); + String localMapVersion = data.getDockerVersion(); + viewHolder.localMapView.setText(localMapVersion); + viewHolder.cloudMapView.setText(cloudMapVersion); + if (TextUtils.isEmpty(cloudMapVersion) || TextUtils.equals(cloudMapVersion, "未知") || TextUtils.equals(cloudMapVersion, mContext.getString(R.string.rviz_fmd_cloud_map_version_unknown))) { + viewHolder.cloudMapView.setTextColor(mContext.getColor(R.color.rviz_fmd_status_error)); + if (TextUtils.isEmpty(localMapVersion) || TextUtils.equals(localMapVersion, "未知")) { + viewHolder.localMapView.setTextColor(mContext.getColor(R.color.rviz_fmd_status_error)); + } else { + viewHolder.localMapView.setTextColor(mContext.getColor(R.color.rviz_fmd_status_normal)); + } + } else { + viewHolder.localMapView.setTextColor(mContext.getColor(TextUtils.equals(localMapVersion, cloudMapVersion) ? R.color.rviz_fmd_status_normal : R.color.rviz_fmd_status_error)); + viewHolder.cloudMapView.setTextColor(mContext.getColor(R.color.rviz_fmd_status_normal)); + } + Pair p = data.getMemUsageRate(); + viewHolder.ramRateView.setProgress(p.getFirst()); + viewHolder.ramRateView.setTotalText(p.getSecond()); + p = data.getSwapUsageRate(); + viewHolder.swapRateView.setProgress(p.getFirst()); + viewHolder.swapRateView.setTotalText(p.getSecond()); + p = data.getDiskUsageRate(); + viewHolder.diskRateView.setProgress(p.getFirst()); + viewHolder.diskRateView.setTotalText(p.getSecond()); + viewHolder.cpuRateView.setProgress(data.getCpuUsageRate()); + viewHolder.cpuRateView.setTotalText(data.getCpuUsageRate() < 0.0 ? "未知" : ""); + String load = data.getLoad(); + if (DetectHtml.isHtml(load)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + viewHolder.loadView.setText(Html.fromHtml(load, Html.FROM_HTML_MODE_COMPACT)); + } else { + viewHolder.loadView.setText(Html.fromHtml(load)); + } + } else { + viewHolder.loadView.setText(load); + } + String runningTime = data.getRunningTime(); + viewHolder.runningTimeView.setText(runningTime); + viewHolder.loading.setVisibility(!TextUtils.isEmpty(runningTime) && !"未知".equals(runningTime) ? View.GONE : View.VISIBLE); + viewHolder.btnDocker.setVisibility(View.VISIBLE); + viewHolder.btnConfig.setVisibility(View.VISIBLE); + viewHolder.btnDisk.setVisibility(View.VISIBLE); + viewHolder.layoutDiskRate.setVisibility(View.VISIBLE); + } + } + + + //返回Item的数量 + @Override + public int getItemCount() { + return mDatas == null ? 0 : mDatas.size(); + } + + //继承RecyclerView.ViewHolder抽象类的自定义ViewHolder + class MyViewHolder extends BaseViewHolder { + ProgressBar loading; + Button btnDocker; + Button btnConfig; + Button btnDisk; + Button btnReconnect; + TextView ipView; + TextView rosMasterView; + View layoutData; + TextView viewFailure; + View layoutReconnect; + View layoutDiskRate; + TextView runningTimeView; + TextView localMapView; + TextView cloudMapView; + FmdProgressBar ramRateView; + FmdProgressBar swapRateView; + FmdProgressBar diskRateView; + FmdProgressBar cpuRateView; + TextView loadView; + + + public MyViewHolder(View view, RosHostAdapter adapter) { + super(view, adapter); + loading = view.findViewById(R.id.loading); + btnDocker = view.findViewById(R.id.btn_docker); + btnConfig = view.findViewById(R.id.btn_config); + btnDisk = view.findViewById(R.id.btn_disk); + ipView = view.findViewById(R.id.ip_view); + btnReconnect = view.findViewById(R.id.btn_reconnect); + rosMasterView = view.findViewById(R.id.ros_master_view); + layoutData = view.findViewById(R.id.layout_data); + viewFailure = view.findViewById(R.id.view_failure); + layoutReconnect = view.findViewById(R.id.layout_reconnect); + layoutDiskRate = view.findViewById(R.id.layout_disk_rate); + runningTimeView = view.findViewById(R.id.running_time_view); + localMapView = view.findViewById(R.id.local_map_view); + cloudMapView = view.findViewById(R.id.cloud_map_view); + ramRateView = view.findViewById(R.id.ram_rate_view); + swapRateView = view.findViewById(R.id.swap_rate_view); + diskRateView = view.findViewById(R.id.disk_rate_view); + cpuRateView = view.findViewById(R.id.cpu_rate_view); + loadView = view.findViewById(R.id.load_view); + + loading.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + } + }); + btnDocker.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + RosHostArgument bean = mDatas.get(getBindingAdapterPosition()); + listener.onDockerClick(bean.host); + } + } + }); + btnConfig.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + RosHostArgument bean = mDatas.get(getBindingAdapterPosition()); + listener.onConfigClick(bean.host); + } + + + } + }); + btnDisk.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + RosHostArgument bean = mDatas.get(getBindingAdapterPosition()); + listener.onDiskClick(bean.host); + } + } + }); + btnReconnect.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int pos = getBindingAdapterPosition(); + if (mDatas != null && mDatas.size() > pos) { + RosHostArgument rosHostArgument = mDatas.get(pos); + if (listener != null) { + listener.onReconnect(rosHostArgument); + } + } + } + }); + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostView.java b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostView.java new file mode 100644 index 0000000000..5bcdcfef9c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/java/com/zhjt/mogo_core_function_devatools/rviz/widgets/ros/host/RosHostView.java @@ -0,0 +1,256 @@ +package com.zhjt.mogo_core_function_devatools.rviz.widgets.ros.host; + +import android.content.Context; +import android.graphics.Rect; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; + +import com.zhjt.mogo_core_function_devatools.rviz.R; +import com.zhjt.mogo_core_function_devatools.rviz.common.coroutines.FlowBus; +import com.zhjt.mogo_core_function_devatools.rviz.constant.EventKey; +import com.zhjt.mogo_core_function_devatools.rviz.model.entities.RosHostArgument; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.SSH; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.call.CallerSshConnectionListenerManager; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.function.listener.OnSshConnectionListener; +import com.zhjt.mogo_core_function_devatools.rviz.ssh.module.SSHHostBean; + +import java.util.List; + + +/** + * ROS主机展示View + */ +public class RosHostView extends ConstraintLayout implements LifecycleOwner, OnRosHostClickListener, OnSshConnectionListener { + private static final String TAG = RosHostView.class.getSimpleName(); + + private RosHostAdapter rosHostAdapter; + private OnRosHostClickListener onRosHostClickListener; + private final LifecycleRegistry lifecycle = new LifecycleRegistry(this); + private OnRosHostViewListener onRosHostViewListener; + private RecyclerView rosHosts; + private View hintDisconnected; + + public interface OnRosHostViewListener { + void onUpdateNotify(); + } + + public RosHostView(@NonNull Context context) { + super(context); + init(context); + } + + public RosHostView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public RosHostView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + lifecycle.setCurrentState(Lifecycle.State.CREATED); + LayoutInflater.from(context).inflate(R.layout.rviz_fmd_view_ros_host, this, true); + rosHosts = findViewById(R.id.ros_hosts); + hintDisconnected = findViewById(R.id.hint_disconnected); + // 取消动画 + RecyclerView.ItemAnimator animator = rosHosts.getItemAnimator(); + if (animator instanceof SimpleItemAnimator) { + ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); + } + GridLayoutManager linearLayoutManager = new GridLayoutManager(context, 2); + linearLayoutManager.setOrientation(GridLayoutManager.VERTICAL); + rosHosts.setLayoutManager(linearLayoutManager); + rosHostAdapter = new RosHostAdapter(); + rosHostAdapter.setOnItemClickListener(this); + rosHosts.setAdapter(rosHostAdapter); + // 设置项目之间的间距 + rosHosts.addItemDecoration(new SpacesItemDecoration(getResources().getDimension(R.dimen.dp_4))); + + FlowBus.INSTANCE.with(EventKey.QUERY_ROS_HOST_STATUS).register(this, this::notifyItemChanged); + FlowBus.INSTANCE.with(EventKey.REMOVE_ROS_HOST_ITEM).register(this, it -> { + rosHostAdapter.notifyItemRemoved(it); + } + ); + FlowBus.INSTANCE.with(EventKey.UPDATE_SYSTEM_RESOURCE_RED_DOT).register(this, it -> { + if (onRosHostViewListener != null) { + onRosHostViewListener.onUpdateNotify(); + } + }); + FlowBus.INSTANCE.with(EventKey.SEND_CLOUD_MAP_VERSION).register(this, it -> { + rosHostAdapter.setCloudMapVersion(it); + notifyDataSetChanged(); + }); + } + + public void addListener(OnRosHostViewListener onRosHostViewListener) { + this.onRosHostViewListener = onRosHostViewListener; + } + + public void removeListener() { + this.onRosHostViewListener = null; + } + + public void setOnRosHostClickListener(OnRosHostClickListener onRosHostClickListener) { + this.onRosHostClickListener = onRosHostClickListener; + } + + public void setDatas(List datas, String cloudMapVersion) { + rosHostAdapter.setCloudMapVersion(cloudMapVersion); + rosHostAdapter.setData(datas); + notifyDataSetChanged(); + if (onRosHostViewListener != null) { + onRosHostViewListener.onUpdateNotify(); + } + } + + public void notifyItemInserted(int position) { + rosHostAdapter.notifyItemInserted(position); + if (hintDisconnected.getVisibility() == View.VISIBLE) { + hintDisconnected.setVisibility(GONE); + } + if (onRosHostViewListener != null) { + onRosHostViewListener.onUpdateNotify(); + } + } + + public void notifyItemChanged(int position) { + rosHostAdapter.notifyItemChanged(position); + } + + + public void notifyDataSetChanged() { + rosHostAdapter.notifyDataSetChanged(); + if (rosHostAdapter.getItemCount() == 0) { + if (hintDisconnected.getVisibility() == View.GONE) { + hintDisconnected.setVisibility(VISIBLE); + } + } else { + if (hintDisconnected.getVisibility() == View.VISIBLE) { + hintDisconnected.setVisibility(GONE); + } + } + if (onRosHostViewListener != null) { + onRosHostViewListener.onUpdateNotify(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + lifecycle.setCurrentState(Lifecycle.State.STARTED); + CallerSshConnectionListenerManager.INSTANCE.addListener(TAG, this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + lifecycle.setCurrentState(Lifecycle.State.DESTROYED); + CallerSshConnectionListenerManager.INSTANCE.removeListener(TAG); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + + @Override + public void onDockerClick(@NonNull SSHHostBean hostBean) { + if (onRosHostClickListener != null) { + onRosHostClickListener.onDockerClick(hostBean); + } + } + + @Override + public void onConfigClick(@NonNull SSHHostBean hostBean) { + if (onRosHostClickListener != null) { + onRosHostClickListener.onConfigClick(hostBean); + } + } + + @Override + public void onDiskClick(@NonNull SSHHostBean hostBean) { + if (onRosHostClickListener != null) { + onRosHostClickListener.onDiskClick(hostBean); + } + } + + @Override + public void onReconnect(@NonNull RosHostArgument argument) { + if (onRosHostClickListener != null) { + onRosHostClickListener.onReconnect(argument); + } + } + + @Override + public void onSshConnecting(@NonNull SSHHostBean host, int rosHostArgumentPosition, boolean isInserted) { + if (isInserted) { + notifyItemInserted(rosHostArgumentPosition); + } else { + notifyItemChanged(rosHostArgumentPosition); + } + } + + @Override + public void onSshConnected(@NonNull SSH ssh) { + if (onRosHostViewListener != null) { + onRosHostViewListener.onUpdateNotify(); + } + } + + @Override + public void onSshDisconnected(@NonNull SSHHostBean host) { + + } + + @Override + public void onSshConnectFailure(@NonNull SSHHostBean host, @NonNull String msg) { + rosHostAdapter.notifyConnectFailure(host); + if (onRosHostViewListener != null) { + onRosHostViewListener.onUpdateNotify(); + } + } + + + private static class SpacesItemDecoration extends RecyclerView.ItemDecoration { + private final float spacing; + private final float spacingHalf; + + public SpacesItemDecoration(float spacing) { + this.spacing = spacing; + this.spacingHalf = spacing / 2; + } + + @Override + public void getItemOffsets(Rect outRect, @NonNull View view, RecyclerView parent, @NonNull RecyclerView.State state) { + int pos = parent.getChildAdapterPosition(view); + if (pos % 2 == 0) { + outRect.left = (int) spacing; + outRect.right = (int) spacingHalf; + } else { + outRect.left = (int) spacingHalf; + outRect.right = (int) spacing; + } + outRect.bottom = (int) spacing; + // Add top spacing only for the first row to avoid double spacing between items + if (parent.getChildAdapterPosition(view) < 2) { + outRect.top = (int) spacing; + } + } + } +} diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/color/rviz_fmd_selector_txt_color.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/color/rviz_fmd_selector_txt_color.xml new file mode 100644 index 0000000000..fb7f596822 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/color/rviz_fmd_selector_txt_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_camera.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..04e64640e17c0af92044b73fe35651bddaa04559 GIT binary patch literal 2917 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 P00000NkvXXu0mjf^rC{u literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_car_chassis.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_car_chassis.png new file mode 100644 index 0000000000000000000000000000000000000000..b237e72663c3f88d2f7fe94e3c813b6161216860 GIT binary patch literal 1946 zcma)-c|6LNnGXvr5j4}dS)-2o1&A5rB16L2&f;VA;N1d$*D2qbKau(k1wE9E`3 z37h&ZUS-ZjnKdDPBs+9z}4Gh>zh~_rkowWJ2PX-bBbJam7 zuEd^y+oY@ZrR;@53w^tEX&sA=J+l*E@zlU~ z#=rBDzd+2hmfHu*J~dVkx^7IEW#ug9u5Q|)>ILtl?VIbp9WohYq)V5w_{-+W$jaP%fFr{to&N?AC0aq*pqnmu9ecalzV-0z9*M&}IlVMu7L%kG!b%c9h{W`xMb;akX!g_1&O5)BB%~{47VtJ! zBRm4Y67hv!$drth6?t(?MlzTn=$MTB2j~_X*HvKnp+x$KugUIVG#<1f7hMq~)Dey8 zZ!=VEdQ0=>mN2Th!mSt4HU}~0Cz{AdH@7^bvn6xfYn|R$nG(F`7sJ2u)D*d&1qp(VaY#V@*Z`IMi_|&vEe=O`;&lva$Vv)Z2`OtU2sb zZGIh!(b~Xrqccrvkd(TcBLv{l;4d2Jy=X|?&z|YvM>1)dZ=6|u8I7kuqr~D^4ODSW4ePm^(l2W~F0i25=|ia5jok$A z9!@5@wE3khRk{wjqVmHCC)XEhtA~g6+K&C9 z{c?#nNm<*`#bODTa&4)M!(D#8fxE`@>C(*+6HQ}N8)zGD5;$dVz!i+27b}z`$zU;ly8TG9(7)2! zhdYWG)(vBGm7GV-ruGE?1r1r&?-!I?;y)4v=`xIHjQ+=XN*BIW!_|3Qq&iMP4^t3h zmy(=qZ|S>7$=rd-Pr0V8j9C*FqIy}%gFYXH6iB0$Dfbe*U;{T#`KfgX(=Ya>dPdGq znM^V_*Y7h^J;!963?_yws>GP{YwIOEhc~(Gr2{V!(jSjII&V>31SGHLbG0BIv8x&}vam*q<{HwdAILQ->eU40iDdHVs{g)WZio{_kHOgN3J zUkfIM;-{YXZvWfUPg0Mv#<=t@DQo*Tx$(KHg9%mg?R!`Ac1+pjYPClvEGst_D-kOr zkAlqftMBotG4Q9rO-kq=@_ksPGOaU-3=%~YNV+L}=an{s83tFJAZj1wt=7;3D3k*_ z3Wu%9G3$vn9eKzK;wgcxcM6T4QRN(cY(=nKf>xY^m659kL?h+vmSMPgy|Lr9DogJV z*b~evuCH(zBN~WuJt7U zzro*j|HZ&b$0EDhFpKgk`&R?+gBUU|>x)2hf*vRSOL{gZLbNtL?p8?} zjcqnFz!o@(e5+GJH4qN_LMV$uniHpu0z>^haa8vZl5F&I8BIE>d&912PuF=lAldeD z!QhYKCxGflGb9%vX~WBD((9>T^{^Oh%cIPohs-EgVRlw;mCQWvO3qIwfg0-@@)Aj0 z`wbrKMcQ-NaHw0XjWQ@HUN5%u+*Nc-dNL{AK3P zp|gy=g)l?}nlC~1Voy?5y?SXLJfd=H;($=w28stzsI^&R^?|NXAe;}Z*Ve)HPL#_FH{}B|!sNvOI zM3?SEvPK^gRtjUDJ$2tH<98rgU8Yp}J9f9<|2gyF241V=xI23=3ag2%ehE}Q<@Jcn zRC9bJg_A`jCNFu~N% z3FidPl|`3?g`5VvNeQv)wY$~*RPJ|1PD&=;%Iegq^7aDOLRmzCdiH^pePP2j`i*gI Q;C}@n>|AZ@;Q^_C0T0NNI{*Lx literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_hd_map_version.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_hd_map_version.png new file mode 100644 index 0000000000000000000000000000000000000000..50f134387b3a701ef95b6ed031660326eb90e438 GIT binary patch literal 3053 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_ipc.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_ipc.png new file mode 100644 index 0000000000000000000000000000000000000000..0f06bda9ca813cdb365f578af9945bf9dd10ed89 GIT binary patch literal 841 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm4M^HB7Cr(}jKx9jP7LeL$-D$|Sc;uILpXq- zh9jkefr06>r;B4q#hkZyH|9NxkZ5~&pG}Ze_o|QS&6RVlV=}c@dF(vckvZANJbEJ+ z6Xzz^S4ZQj?Tu#H_)GtmVOpQUSyym*t^9tGKj&|KuVeT#tHrftLx+Hv;2|Za2p5iS zjzmF49i?Z@{d?+eo-4as_r`7c(eGDZKP>s0U!O1bcAxE?oZ#2tp=V#c?%xQcWnaJi zJms_fq=SiP%kKV5yeo4oucZ3>o$51FzV7~gc41bf*&0otveMVbHJ8_G=(WOaz-n!a ztBdg$?$u`($=VtPhh8)8EMJ z>sPJ1TV(q34}VHQ#M_5yzc=K$wO{>yF>rIn_u2M&?)yL7{hJoL)kgZ-c22P8+!J}9 z1bJWyM--Qu-2L_O^aAC>%IfyNBm6=oXaD49>p#0>{*~<-=lb`~vEA?Ym!0!*QVdro zMs%EAba(&vX1M>r4827!ci)xnF?|15)JEc9_!|seZ1)`!Uh!|?t=yk;O77*?_IC$o zA57fZKKt$AU6QrllWy;R?*BY)1~3H7>;L_`eghO?tKOdz4!*X2l~hf&L=4wSba$Y5 zhoieA?SuNa>-x8Eu9LPcD~hU#|LSYL1!(?_ul?8Wulu!3I_>M%<2SypUe)*YOXaDb z@+Ti`oOw$({CJhs??ZF18W()7vWS&^o%v_a<-D8K9R_;Gmrj5rKXiNQ8ML+bRA}5Y Q{R#59r>mdKI;Vst09he%SO5S3 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_ipc_error.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_ipc_error.png new file mode 100644 index 0000000000000000000000000000000000000000..d740811a8e4d56bce5602528e59b30645b2b03bf GIT binary patch literal 826 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm4M^HB7Cr(}jKx9jP7LeL$-D$|Sc;uILpXq- zh9jkefr06Xr;B4q#hkZy_vYR9kZ8N;KT*=jsYk?fs`Q0*u`4hBy{>uZsQvd1noOKV z8BTs(IvcKf#T3O`cAU?2J$K;}v%rM*SvSvC{ayK>{@b&q`VIFdZ|D#Z6Fj8k6yd_r z&54dgSN@=IOuxelwU~o~&N=`Q6>8$@jPa|MqK9=C0>=^JYu!U$yG(uKmaQ z;zO@)eS2K<`F^$L!&YaW-~Iab?d2Pt@62XTKJFv=ziw;Qs;8eaB7%>tU$rZ8{qi~g zR_zcK0$b36%~puu=7s!g^k$`fnKX0Ns$lP^bB4z)|FP9c?a6s2lb=<^mpG?CS`pK| zSM+%qfuRKl9y9Ldn#srgQu^^X=EqqLAsbF#EOxTxD{RP^mG|xKZJ@LF?XUm;?aK6x zr~lUdUJdlx)_6Vr<#Ybe;^BK;x&3yWy-g3_>nr>F#C$LR{rJJ)SM&b(wR+h*ZKSSE z`M7#j-nHyR-X~!mSS)FAMG`!dckQ*mGoO#X< zSM0N|>@YZYeC-5ST);FxO1fxr*E^mC?nMxzKx}jH?Y##j#Fy8x@*n%qVT%zm4|rLY z{d2fI;r;1lXC}(kzmWN3VzCAoiPg2$)>Ynt;~Af zsrfiOl=rS2+jb`>6!#<22+UncA6~Wne0ZU5=Ps`}@7kTas;qxKIjm@17kcwX;f{le zfA^|9eX(iE;$Qz>obIo^A9maAx^-#pZ$nGF{{`AAD5(x+;v;s8nVYMBPghz8@}j4! KpUXO@geCxq^=Uc) literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_laser_radar.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_laser_radar.png new file mode 100644 index 0000000000000000000000000000000000000000..1faed3b1aadcaca11cb2c53f9a8226f386fd2056 GIT binary patch literal 3380 zcmV-44a@S0P)Px#1am@3R0s$N2z&@+hyVZ#-bqA3RCt{2ozHLLIuOSvp$Y<(6MK65;~K<2SgEI+ zkoX4?To>M+x+kh4B89v|8D2=8#AA;g$M`F)^x6kaa6Zh8$74?f53XfaCIA4%WH*kA z=S1|HGQW)Lzl)OieD|8sdyfA-I;H1L7UlK#Jd&cmZ@`uipiA=a41XsAhFeh*pAov< z%i?JIdalT8u$EbwAV@YKlGfzo2;etpUV-H-NG(E(=9~K*JXlp1Hx!tfgpm1Q$p#A+ zTBo7I)Tn{G%fW$_uD3HdO=sP&^hlGBT^hRm(x%0A3@#_Yvf=g+ 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 KMNUMnLSTaQ4nWcX literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_launcher_eagle.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_launcher_eagle.png new file mode 100644 index 0000000000000000000000000000000000000000..ab3f2f7babfd77e6b6b9733e7e7d1aafae05fb6e GIT binary patch literal 3254 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_map_verson.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_icon_map_verson.png new file mode 100644 index 0000000000000000000000000000000000000000..609b355c24d7fb3c37babfc2aa45034a3af642dc GIT binary patch literal 2075 zcmV+$2;}#PP)Px#1am@3R0s$N2z&@+hyVZw#Ysd#RA_5=4Ss`~bY9CJBL7YFx5z zK+SB5A`@XE6SV~l z)Byv=HZhOjgt!Oqzs#=J#%~lpX@z%o=AZA|nfd3R*+GL6qrbnuU!&2S&m!wuB9XXq z;lhR9Qu3)NrSYp*ueRR5fB*dG=qLc7zP=u_Kp=3ulze~^3*Ok+_;7V~ z6@fqi01yZS5DW%!^5n^syjH6fJkKBVdcCEj1Z_#Jt6*kk217$b=^no+b^T=*3;-UF z2eY%Y+fnuQ2nGPB)4468OE!iVPIU=ay}r*gZZ^-&G2iuK^3vo} zsH?@+J^ym`OS+$4iSgAHy9|R1@)qNH9(uhV0|NtTZMCm zjneOZ)p~Dz)tA8$$N$`_b{ z+aqVdFJzN9t1jJXZE~|hJL($HQP&_nKUTk2N@w9W>Cx?byrq4Q$Ai6l_oB{R3#V`* z)8)-LNE^pQ18C$rk#nydu%y*6MFCIFAOSZNx1rS2jRru~z z7`JcVMs;;{#=KUm72kKB!qJIf*7kv<=sw?&B)+S~b}p0XjDqdP+Q@JuWXM%0i$6Yi z57S0nR(U^uz6g6Dlvh1{z9C#0eL}Zsb*1gnVmB^GEC85S*Ku@eKC8UC^;db?VAY5duNt`^(Ulkmb{ zSrrsLa^h{iXjV}oU0+Xh)HM{;m6doJT|}$Fob$7M@SFc{{66(BprS8j42zx_muzh* z(@derWUrWY4b0B~khI7=c1kSmS)A)BHi2@3dL|nU5>yutZ?jm!#Y0Chy%OV<=_!JT zBO$|15DX->DAQzuX{AE${cQ$yYo)iA38p?@tQPL`rQO*mYMFFU(;*W)y%OV#*`2S9 zKFNB;10>^OCOj+{9d!*U<5(tGtC{)A%iBu(91BE$lAJw(V(^6YP8}3D#=x zEwKG_aIx#Zc<6}iUK0E`G*|Tb*D0LHNkFmVcLM9UB*CQ!IP$SqiXO%~F4-8~2?2)^ zq=_7tmc7}DpdLexOA7#pYD{+a;ZBAY0Dhf#_Qg&X%WBgAfTe2p+RKfNs*j4e>!8Ha zt2k8I@Y?@>pz1Pg@`f<>q>!m|lrtU$wsMWz{gaFiSDLrUHNfAmf$} zEdX+PA%hFEdB4D1$|@HpYUKtOX3^=s2Vjd&EsD);$zUn=yYfLge#*}^3|B#W*kC@R zNW|d6EX;Rqs4ms$Z54Cfnk)KzLmBy*@7z%3*J*9))h=(mKCTo?&EIKldW587qwBMS z`kF}~xz^nd(d?R8jh~p7vKA+e+IK5<$<~V`BF!gA_e5-|sjJ0yj*ONY#P6`dd`pd= zGvF79A2l4Xxf!5kV`%ycr!dxw%^m%R0EC-kzU%ZR%h}4E?t2oyk?7(XBpvQUhF>p` z{7A0GZ#WV%NU=0^6}z!E${0%k=wa%l3VNLZztCxIdQ`4frm5wizGhP7*DG=nm~1^F zXFiPsEnaiu`&q@O!A_M_(hfbatZ8 zHw3$}HY$Fa=mC;vG#n0xWbpLq)8Kh&X(<57v0jc=CVl|iRQDGAdWau6FT1fess%Q? z3)8xiK_z&e2gh+_@E1BW{i)I1HjKC(ZwbJUyJ?; z75l37Uhz;!bT|?MfEoapo125n<$}ZEkQwuOy<`jk_5;bT5{a)H#+R3*e9zZbU}Elb zfwF8Qz792hUavQ$m0d0ufd4fHP2}mY)XFN)e~_3g`MJ5>d-f~q zNZ5_F5&CU1nWWP&9LGr)&+}5c#bTk~5X+YIR8BVhIF6Ijbvm6IKS2v5qmdV{U(sn* zcQ_mdARi|{a#E*hKzz%?0Kk+j8JIdIi62E;1c$>R@mpSAmhn4mFy8{;t^feVGAp^U zXR)YuU7qI=3ZuX7=BAierirj05Urz=GobtSQiIp z0kFYO3K)KZ@>#mirI!K7CUpYyUng$yDmSEh)&2v9NxPh@VVMKa>2xxufHDOfX&_*0 zeuWuTTtdN=3x|O8{CW#9`VfG>P(vj|FOuB+NZkQR!T+23O)oOO-mL%t002ovPDHLk FV1iPx#1am@3R0s$N2z&@+hyVZ!5=lfsRCt{2olS4zObo}JunM9or`^*&?Os8A!AhKR zs>Bzp;M#WgR8Coehy-~KFxeqMGB5VL1plO!RxKp~{@5Pd<4K^W%RL-}{f z{xNGlzA6vH=FfCVvUGRt=hfhEDgHm$PR<_lmM7)qU&K%1`vz>79g@33V>jq9(2Nlqfw^#pZ6a$N@leau6m-DB!2oH2j z$4hs^HIgj7+m#;%y6+veJ0ug<*8_~^Pr-Oo$~bYbbbLG0@@Cp6q4MN!&o@Xl^93@EbjsKU}sGq4+^L&qIbWZ}J8 zEOvtq&~S&?X$Mqc>BLv;2Kx=FuvASd=dRqKwK_yjJD>@RV#&%N;K?1L35(qzBjY7_ zXE*2$U+xe^SUPD2UJbfO=RL;?X}G)L4$x_(!OFs;2#e#Zu+iOUfdPuJINKgJTJJeh zoMkO69iPncl|OXedyKS0=TH#&Af#&?_P;Y1bFUaXj&at5+5&6;n6=FI!+XasMERZMP5O zTW<40SX83o`k;SEV%!_zIujO;%kDQ?Tf=m?BCa!GQI5;z_^RhL3{ovDp0&__s7FM} zR;v~k^>BZVukH(HwZh3_Ip&@%zmOk$BDuo^!0o9KF&>;9D<96oS)H zXF~vh>FZ~(XBx*!ZIz_twoVe;7BNTu8-TmxNScUmk;vrAhi3z@MN>ftskTa6zxN~d zD1HM7=P)5zXaE2#wtvVq+cVd85|T@+)P}gPB(zhxR1jj5&(Z;iUv%SQM|apN@J?azYS1AXc_M61Vp{{? zY#VEhu{geRz^R-TYR&d$MW*9R&}y;vqt&o&=o({zN9&}6uw=R=KMvkq6N#XjTnMb= z@lJ$QFOBaxUW-&vO=N+!J&smbSKdq?^8~!p6H&={%VQ1T`RncF^5Ul(Dgo}GRi}r7 zQiViVNtW*3=H2-h)4(uv#6+EIU#xq9L|D3GA%(ztu`7GDDVBF?rDTFy6`tHi5p7f9 z9lO3JB*L;SLoDY{QMpH-uJ6`d2mrtHDq1t|rmks~|N3KoOE~x7iX=;S%lXsV#kDSG zVHqF_+EjJO`Z{gD3_)0eAl;(_*C$Wtqj>(>+9HcA8esthmYyIMl^08m_?m(W-n$U1 zNAK2)P*k;`e2jeDK8z>%`xwKDMY&67Ie%Jv+hj{5s>xzGp5*U8Lx?9&1BidvjPH(8 zSOBNggllmTh$$Uhup zQPmBUo=0cvP?d+_B!B;@vXKHHa*G8(e?ohbr8`VfO%}^>%w4&vu@{N3T%MUjolV9} ze#YYZI%zpsRX`#vmpi8^MJz`0U_dFVBujT~|0u2r+lO&m+!cR}Ma5`?R{KL2)oC@X zQ% z4;ZJ&z+){4rmvshtF7hz5q&|M`v`4BR+We0WU(B}_er`cw9VdjLG|1itG3BvX=e>a zn_`v!`s2dtzvW4}oIkAx@cd;QlQjZ*I#<{Lvx;7yghjc+val2}gb;0D+`ltn$>>Z< zx%gX`yL${iWYgW_RP8IK*to|5Jb!r+R!pd7ngvFSi2$tBElf2Kede7SUF)N}ktAeM z0hks02k`vG!cstIk?@u$rSb<6xTpkL;u>v=w>&9XSSl!#Fl3ZIh)6^wPxw^z9CrP=aqroZOQtsy;!1LEdjD{qtvz(E%Qr^SI@%Q=r z@psY+93ykTdYKlJ#qwfvN3*WVt3q8gg>m*CB8bZNGr>x0biFPXmI(&pO7@SLX$ub3 z7BE|pwY@GDmLaXy&(6@E(33y(SixEt>hQHoS{UfFLdw76PK(*M?w3C9o713kw0; zcq=$j2`t3L!otGvLkLz>0>jtE=>|*;`uvmqV^+bz{F*m*?8tGS?WPHn^D0?b$mr&` zmh-3eB!B;@a}6<-uPv$B5VzH)3GE&;U{{EaQM{$oNH^LP%lXsVBvUSLno#{4(+UHZ zbf~Z?RwF#03Y~4#7T08pMTO=UvbEQVsDG<%fSv6y6c!dCx-+G&evrhSrndxbI}C+| zMTm90<*0qRDK3m*C@d^$O#9^>_r%NB#prP0;N1~aBg#W1m^_xq&G^0#DrR|=g|f@p z1;GfbE8)k4Mb{T47-2DXi2if=XH;kA)I(WL2bx}kg%vSpQ+Kuimo>+S;T}H5)7MYj zNk7LeZLQvgSA~&MPhVD8Tw%5ytM;C;up-2GwQkwGr!!2Bg%ugrDIh~}&GIVKxh@Oq z4$z?XpSg6KZCQW;`dnLPFp^H;YeI3Kw15{!r zinyxF5OfNQx-g7=9^k#v8E+B9RVr3eDR{R!3Zn&+r7v^I&3Mb8S+FZ#rTUq*Hu^OY zJ>0hAbyW7nG5W~6ZcM(kD!X=6yCt-Q#e|R8{OW4r;4Qdqrf-~FBGw_Eru+{(xA|8J Snv&-L0000Px#1am@3R0s$N2z&@+hyVZz;Ymb6RCt{2ok4HnNE3!%n@>hW%W3Aco6Hr&AB>cf zPm%b85nPANoRSkG5SGB-A((L-gS)D`x~mPZw1=HQ==hF(2O27ijdcCrJ2mpmyX$Ldo_=mBswnJ>n`2h28ru2g*y(&E2$ ztl*=8Tv@3!)Gj4ix~nWc7U=3BPv(m;ws+&u<7iVXz*Rz?%opR(7E4uwi?4Xc)3%9Aju!BIp8n`uuoU^?{4gt8C^k~XwfENO` zH^|~}N1Ky72C3K|L*d4wtkM} zFQ4UQw#VwbvtM`w8#0KQsdKtZSTfasS{UhOXkiu13bL- z**$lO9h6xZ1`9F=u&)$e3}VmIOweS=oNwX{aJxA3G{+N?=j227>t?}@w0OFI@7e}! ziu(KkB4lcybpzZjD(JE}45@*bF=XmId;@GQ@^d0a+~$@{47AeXzpGjz*=m4?H_{sj zJd7YupI^UGKL6U6NgTc8NkIi|YR#RHXL(60gBfjlou-u3OP&;@go&Zs?u1N<=8`Pk zU0Wv0wU&?5H3f9@^k(a8F8z=aY-tU&>cx5Wf3J?H0y>YWPV%4s8o#(BN%FsoZ>CJ{ zN&>Bu`#O$&j}?yp|LJvmtWJO1B>$=Ji0WvwHh%seuRHtv&suU!R@?;ZLK?O~Nz2v? z)_fK+CFQovfBZHYh-3LE)DI1t;(fUuB_Mh+2uNHpPx&5zGuGlUm#Pg*k`ac%gbz%|NJ-f zLAkOx482>mr8R$nOtE5L^VDR%7&|?QVX4ZheSvmf{-e!`H6L@`WA)v(G{;z`*4*-} zK0!A9X8N*X&45pGP=jS6iZwUK^9i!4t1btB;sHF`tTEcG2Y7gE{)f?Kon+~bx^e$S zo#vR`9xKEa}*u?FXz>qU;ip;%(*B~Oa!^Xty!OE{S? z#?`YpxocBbEgf*NiqJ=!^)mnQ+tr$V`8ZuK^B=!C?orxpbEe7@WYY&LeX8GAnW~AX z+kODV=V#-;wdRRZfEHD=MrJ5#CE@@NZ_*jJ<>PcsF3k~^DTj(HsajJqs-{}8&k`f4 ziVdzzIaGV-DPV3vl2J9uiUX7wkzKc<^*zawr5-GG*DF+s5f!wQHB;y16fg_AB$F%ip<#~1$5Y~gmpnNJUWN}@gC&I? z1F3PTl4=$e(KWR%MkvtKwpUWFnp!fI0=)Az&ZX#tLYroJnQ3WWM~w$gmP|cg<64{( z3cl9P)Se}hR?IoK_V-jH@qCR2`RLifIjbyb#hh*sC&;GjWyyT4wxgolTXQrlo9aA2 zwOurB@h5y9iPRysE=S|yb{dfGF-p0~r{03`b2|x2XI4QC@bJd!+qJzLPoH12a`2}x z>=|;>EHAVDXgS38Zj3GYlL5#tnM~VkG~+9B&kz8et#@tpk|)JvzF>XfJ|fS%;U25; z^!arMe9TYH>F85zvArAbvHH?3nIh@)(v-$M%`e-RF1cDXisY@vS#4#bvhb0;DHa9=%Xpk zNtPQTt&k~u%)Zi#FL_dM?lyF;(Pq7Px_|ez7N%tN&PDgFkV)NcJO@ULr~CIHR(H8qOi`LY zy6a^GSxZmJzKinQD8&iJmh9rDv763IvQUubu5Z>XCdpRNB*?MUN5>}0u^Ud0QF`DN zX=87#@=?!ChP-^7t|^-XzHcdwO%ylHtpsH)p(rgV%HlA*x$SmQ+%&*16l6ixeA*!v zQQfr7y&LRg2-R(Cx*Au5sBYS3^T#UVMRgxHxBVesZu<0on>TB_FDVB3bkmirbJtY2 zeeKIC>gh-qd-P5=D5&RuuGN~U%Sl(qZrtrc-Q4y?QRiWox7c-7hEPO4))MfxHlLzC z7uTmFo$e=-fnCA+nH*?=KK*scebE5%>{2Y9?YQDx7wmcr&LLB?VSP?}l5cevx=mK3 z#S>>))i==8KuhwPLa{zxpgHrfWS0WjW^IfnFG3^nCQ8k8CFy!~QOKM__u=^rVqzHMIjMjQ=zfTr$7UiGR|Wxk*U+3~hbF*)B1^8siCm=u8}3>-)Kcl3 z)IkPxfbI)mK#{wyi?gQOWA)vY>t>}Zq37DZrc^@atf2un=}#`;&}-pKQVAJguAyO{ z{_GoIKrv*17jfI9KUwcnknHG^%D}5B?oDfyAczy}X0k@#90WhFE@M;q`7;ElaT?=3!Ldbyq6}+51;aC9!5xVQTIM$eC>F$~U1Cc`p zm~NLKt@&t*qK6E~f$jrfpeK+4_n`X%80ZmXz#Zs50S0;o8L$W4H^4xoOQ=h-0n|sp zKo21UA6tLqTUn1+<_ZldsCwtF>oQJ(M!pX2Qm-KcfA4CnU(%Um>8|R&-)Fd${tr0~ VQ&bdKum=DD002ovPDHLkV1f-YiTVHl literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_car_status_select.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_car_status_select.png new file mode 100644 index 0000000000000000000000000000000000000000..0697d7c98ec367fb9d9c48603104f32b61de30aa GIT binary patch literal 1327 zcmV+~1002t}1^@s6I8J)%00001b5ch_0Itp) z=>Px#1am@3R0s$N2z&@+hyVZt*-1n}RCt{2T+MZ}Fc2ImX@H7C&d`E+?!>qgKc@w9 zHmOJzeCRuXh!DaESpDQBb8-x^&(o!i3&0A&br_@A;8 z)(1YJp{ztLp^>CxVE^fNGJJufU3xO1QJjREWdWFZjzr5|aMgdyNS4iPQ*o(KD9<97 z|8UWX(>WN)Do)~|Zu)H`nJ_)<^PVvP;OgQZQQv6pSik0w-MY{r=knjTZ>BPfr2;b?eOs18YVeyOCEmDeX8voTjEEt( zX7DjETV5f(BsVHqiXaJGWT#yivOfvr=x`}cq6PX1C?;{i$x=DXjG$-G)^0^5tV@v) z2tw9`Ac_xU$W7t^|FgLlvPN+`gg zkEmps(|9Ti<|V`AqrWm z@hZ_2A_`fi3o40c>3S*%PnIPf*(!HZOd*1kWxAkJ0Zi*k5%jY1#?6GG#Ak-)p4CMq zrWPmBx&;3%m5N(HPS)na5~XJLu2@em;X1nL#-f#{g?T%%-wg?7EK`VkCd_Tk_!pDg z1LG!-SzDAyy(k61OqkPua0)Xdxf{w3kK!a;W6Sh!i!1KU<_9+<#cr~iTh;lvV{YhS4L3_6EQKL6_jSd=@g5>8#2U_O2|Lq*qq5L3 z%kr>>Tbu4#f=9=DICTiVhKuBa{Z|vf$9|tG)Yq}8c}WLR$vR0= z>chHhv1zXhQtAC9WO>oejcmR=i+q<062vM6vgHo7by2nAQSKsnzxy47r7CBkFXDmv1YSceS<5)y zebq07Y*P-hZ`j>f*JPj>(PTFK3DtKYp>LY77hJi%s~lWXNgSq>%}ufH&$@cd&J^o;>M=Ph zDzW?~G$uNnrjv~^OSdiwT#A!;{M0_^y(lYD7nP8DIMTP$rl(0-vSY8Go7o#wq zbK|1ZEu>ARhfR{wF^e|k;4L{=hc6_MRE`gQ$1cNC7_w&Y%{{BjbAQ^#I3u_cmPp?X lWB76^)dZ&X^-qqV`VT`ja@uN6HMIZ$002ovPDHLkV1m+}cbEVG literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_car_status_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_car_status_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..167259f53053437ecef26d1e0cbbd50684151d05 GIT binary patch literal 1197 zcmV;e1XBBnP)002t}1^@s6I8J)%00001b5ch_0Itp) z=>Px#1am@3R0s$N2z&@+hyVZtS4l)cRCt`_T9rwlOF#k`OX5Ap&M50OU?T)%PRJ%*-dtHiMJ@`($x($JsqAp+c8P zL+l#5yAPce5(}{-fD+3Rmf!px`?Du~;tJg(70QW59{?ow&-4Z}pVchjm0ZDMC9+<4 zk`+E5rc4+|6G}l!k~15lwI!Mnsc{ z$mE%yF!Rkw#^J!s|J3Qt?+xdk!n^@AA9R88RipG`qri7ZTC=39B=3jM3(|tO-w1I> zy6%yKJ zGwm0PBbX^Y1E*DQhYYzN>z6AQXZVTqL3C=PF^ChWm%k&V4ebPrY>%sT?nh^)!-$q@@7&cc)|zZ)NAz#M}mSrWNEtSMTOH7qGHSo^FOSoh9* zup;Xyp|^L>48v)b!{fL}e^`-q0>CYF#pKBx95%=jTQY0}EU#Jnt&n4y$5 z?kD075_3sDY{#r6r5=syJ}WU3>HqVaXY~&rp@(eYv%cLA%Lyz)rYJc=wCFIh$zo>N zk%v&v!9L&-#%a^GHz?82QZvkMIEH{mgvOD%j@z#%38iE;AHzj7fikNr3Ur6lEF5T! zgeZ^V@)C)?hYgdGrTobG! zUlp<%X%~5IkbmtYs%RBZceqnqyJ{`Pt0q@9vYd9+B#gZC922ZM?=*X^QVf+RcL;t0 z-&C@yFW71^kiMbbBsW67pW;d~Jn>PxLJ|3^m!BCRDM^@u>SO`0>-9RK8BqW*Id%O+ zgGu-;QoB27LX-|!)+Kb$3zZvUEIzC?=7t)z?m%$Yp+l1`2AsCu(C#gBw`Iy*-W$5C zY74@;uIKLhlGPDbBXAV}x85&iyT6fEeoQi}n3?%z@wUfoVo0WznQo(CJpq?-PRnLy zo-Bz1j90P(n_W5xJ6oHK0CTcdc%+fL#�DVi>Ykuv-2x8ZJ;v@$JNT3&;h7PIes# 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_system_resource_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-hdpi/rviz_fmd_tab_system_resource_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..106067568a17795080468b3216a6c2d0f2755e6b GIT binary patch literal 628 zcmeAS@N?(olHy`uVBq!ia0vp^-XP4u1|%)~s$Kyp#^NA%Cx&(BWL^R}EX7WqAsj$Z z!;#X#z`!Ku>EaktG3V`^=(0l!BCh^xK5I$Nygsp0Iotk7sESQd6z`2We-|GJEocw# zDRgR@Y01EMAid&_wA_KKXLNLDJaXZ%$i1}W`vV#8-#hFluFyGWXR-kH{i|Nd> zXXKredNueT>@xZnx^b@saurOL@%9FjfZ=H@`%g!|}9~HhXu}q(Og2(dv@@Y1o&NP29iP{vi zlwBw6@ToadPoH16OmSOCdib?3YrGRH&ljFLBK0XrM$npN^2}Pnk8-d3f2~g1eehgl zUc-&NKUGcll_$9toO#k)wd-PYyS_tkTdSu>e>dI#I9uK$|6I(Uhezjm z{-5A{c!I+w4wY^Mk+4zo@B8o!kC-_gsXI*^{w#o6`++xf;{vscgEwP=DTl$+)z4*} HQ$iB}qrelc literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_camera.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..66526b732f690a7ed313ef349e3c2100b2e71d91 GIT binary patch literal 1952 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 Vt&_A>eb)d0002ovPDHLkV1mE9s|Nr8 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_ipc.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_ipc.png new file mode 100644 index 0000000000000000000000000000000000000000..86edb805edba6a36a81fb8a17f8deb6f81d86ec8 GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^DIm6JcFmrgg*glK7ihD<5!+#cqB3L`{P$%RQ6# zV^`~(w$HuCTC({#+n%UtpE?@MvH0>AhA)#H6L^@C1tgjeNU$u6-d+%ASHCo8$&7Pu z8)f_I>gOG=_!^h?JNxziDa-FEPU{!vW}E%>Pw6HI=jnvN+Lur(TT^uWzY z$=!Jx_LTD^BK%S&miJ0n^Z32zD?ZKDY(8ssUu<{wYk9WM zbys!nUHPi4q?P{hpYvA7pk*fRE+Ut2+9g#8ssPm-$-G$D@icWp^sxZFpnYx6HMuc7 zc;z1d$*Q?aRzKDy+k22WQ%mvv4FO#q{&$@%~Q literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_ipc_error.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_ipc_error.png new file mode 100644 index 0000000000000000000000000000000000000000..10edcc69ed402bd6f478658fedbb3b322e2383cc GIT binary patch literal 514 zcmeAS@N?(olHy`uVBq!ia0vp^DIm|wQ*c86>?7Y;d!!4=-8$q@%+-{MGk$^{35teJZmdKI;Vst01PhHX#fBK literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_laser_radar.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_laser_radar.png new file mode 100644 index 0000000000000000000000000000000000000000..47b710e027f73f190ae4767c082bfed4c8c649a7 GIT binary patch literal 2236 zcmV;t2t)UYP)Px#1am@3R0s$N2z&@+hyVZxW=TXrRCt{2UAu1MIuISn0Sy~*wOj4oY%9Y*7$B9Z z0Qo_~Zu8hGTWJwP0%YC7rk9o|a%M>Ck?2%O6j36N=E30%8Nmp!Nh<*WP%ifKsJs>G zwW>JI>;E5#IKNz<{Vf0hW``u-rcn|4U0o6N{Q`B!!9R+(6@Y%gow4}H008#)>&$!G zju-?Xo3s)^39w=Dkqw2MUI~#z^g@CLk%;qZ_P6MhoX!au8VRlFNoqPHWHdz7(J?Wd z5RyFYR&ZFZx_DMFbVN*0LN;k7N_hG1qpi9tJ3Azo-=#Y%Jv4xmi+s7aycOOgGcYkh z2=Sqwq=EH0f7qmzkaHO)f&!A_oe&qPh(w&n#eW&Vzpp+dlBeBCjdIm#0Ajon!m$;S zguZ9dY&++f6mNvAA78&w)xZqy-uot^4vy4BBF=XYKmYi6zB3^#doiGyaT}--<4e+= z2qD{x5fD)al^9p4XA?ppV<>ili{xpyqIhO5q_7AfNyZ4Jq74*j;gUioI)sk+X@hmn8Drq_CB17QfK7=rDsueZ=BD9Ypq)RgT3e)e?U%$Ec zVo1T-lNj|xgb|Z5C@wZ>CDxCx-%5D--p88BWrofGrtc-Ze6QO*9jp?9@+us?DQwt9 zjR4EPsb8I%odUoJyF2s)<9%YZh>?)cWY{_(4YG@qzzP~N`-FLxO?8=7R!?L^BF<-U z_r81h`Dgd=^N)+irb-hUhvkZ+!uI#;Or{vM!74><7i?_181Xh06)MES4XtzgXjQ?6 z54l}JEFJJohLng9@vi;-I-9}WyZZG?DKvf2RdJlJA78&wpJz4lhvllxz-uMM`b9BS zm5vOba~BtwzG}`p+N7kdO}&Izq=Rel8ZWr`$c8`$z**v3q=h3XwtiK`ajuh+1|e2s zW-(eiXeDldfk%^3=8!UjyLZ!d846O-ln~F-F%~i;Y@|@UuWcF?YWk9{z53s-2nit_ zKFHXlmDr?}NS<~qca#dF`)V-AI@k<+y{#*wZZkyGcuzw*7-Y0M80P_N)Qgxd?A4ra;;w1&uu@xVjfkFRbrfZUBDkGbzj>IGd7aENh@I*Lv>CP0oZjM#h5pOhD@&t zn@FL1H*EGHB?-Y4^3kZ)Aq=WtkFe&0>)T2lcVV z#eZ1|FO7kfi41iDN_YWGGDy9hlgcuDNmO-JcXLVi0&V3E<0#ld;#l?7W(TU`I8V~& zA9AJBb+yxKLs;tx%zJfTvo0bVi8!aI(Mz;f1MKXZG8yDR5*gX_HKpVA0VPi)Yd*j4 z?08dObWD;~8woMp z+&A{=uirW;7)oj?7e=FAUy2KAJ9x`-KYfLbX<=KUg0n91$ZIwO~=)l_tzP=x^?;Eg%E1#)dV~!wKo?+Cbbn1gxZ@c zl{I-={$Z1jdj7!`A+}k&uageG6o_T!4Wn*bjq9Q;`LlpiX5JtKvvCUb#l9>Un=mv8 z!4xP?Qep>p69(-(#ASP-31=|3DA5wKO{2n=wK2KD?XZ@~8OFACrf$pHpl-zSRiNF5 zss_{88bc?x?S;5|6Mj};Hw`siHWGr`fnc&H0N`&2S8H(5=O)3Q6`QmYnCSo}dCyA7 zHjN6a>VmIr;O-U*R#-IY2-(ZBB<;zC3tf)N7XX{+!vE+=*>c}%h#PY#Ukls5UlzS3bsZ>A8G z5DA~e^oEY?TqlG`jFFhGgQcAkA~6PH`iffjLWo52v|Eu+;|z!)EFJVph>E_XW+*$! zUEZhf7tjH_nLY<90RY3>1!9nd)Uin`fpul;Su*Nj#zPx?9sUQAbM|W^fw5@-0000< KMNUMnLSTYQk`>$l literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_launcher_eagle.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_launcher_eagle.png new file mode 100644 index 0000000000000000000000000000000000000000..93b57caa194c4589725d24a3e8afa3c12c4a8474 GIT binary patch literal 1786 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 M07*qoM6N<$f+8T<$p8QV literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_rtk.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_icon_rtk.png new file mode 100644 index 0000000000000000000000000000000000000000..faca03cfa59a47de7c51d30896c04364de43da26 GIT binary patch literal 1884 zcmV-i2c!6jP)Px#1am@3R0s$N2z&@+hyVZw21!IgRCt`-T}^M|JP@9QS3y+e)Sh zaz8X*Ct11$0H_B4O7pY9X0l!6N&S6aNcy(k*N=MM#{^*LOZ2F3HkuTK+Bo0DQdaEU&WNXf^bHctTPcMB*&3vK_qLJKgX= zoOCE+2u*gknbQLmAkv#eXZ7vCOE%%Mz9dVx%jbtr-+OtH*yRF4FnL*iuaUXd0X%;A zXoxe3TmrS!)(1{nS+bxXzY@R5SV1?_F4!8_|Dr0@c4oKpMCaj<)f8E zQhcc=cY#>sNr{C&RkZPonrp0u=wiYlBYdiDS zXCV%13l;2OP!(KhJy=B2mQ0+hGa`k9&BX4-95KtQY&u`x?cnX+yU@D18Bg-pUz{+6 z3relqY~zt6b|r0o>e-r}On)=w((<)4~>npMC~b6(>{*UE2srmTr?Q-NNXrIQqv#RqlrpL}H;_ zGu-(JaV)BgrbJBav-dF&0SU8+TX36GChyfs3Yy_g0-)j64KN$N%K!WY zV5qYufuTiQPLUN$*EneQzE5FP`kO>0Rc?;SwPE@E@EMwUP(*UeSvFv?v3Wo#R-M3} z0)@%T63s%Wgx>|ELwxi4b5deG5u{nCj3c)nny^U+v&HJ?cUF_h^X)cco zYVNPbVZoF9bqvs$Nj&`B4&Hzo0;nM|+{A2g)Q*KJt%t^T%jbtr*I2D8_rt#C7F}Y_ zBkzoPF^SlYy`w1r19$IRz!X4_|GITWt@lIwI!VJk2dNthv~CeC&bEb^qB&evPl3HlH;%*fB^ z+yTk%HZ!)`#C(fq#a*mK;NJG*)(w(&$7mlpcF?yX?mZPo^^|w;b{`9o z+GqTDlNfrIOjqDkXF_{2LGy_m``EjqVQl3My+!blCuJY|bTs4x6xw4bGAS!@XqhxL z9my%g#*IS{Et3Riv%2K=I584A!%JOwY>p$5K5nN@}sbNbG+< WTcO%c4s${P0000Px#1am@3R0s$N2z&@+hyVZsG)Y83RA_nr z_#Ng%oM2=>1cq^<0)o5QSCvYs2>tA)yJ;XyP@hz%H4Y$%=`#xg&i51k*5^lm=PjXRDlG$4*2>OgNe-!EJ`k?KYXJ>X$(6#Lq_ z;A_pw%K$)snVSsNzfJ-G?pNLsLj?eikY)D@nwY|uosJ*s0fOkSytL&P!SSRtG$aE- zEKaUh071O3w@OFdD1pJaaRzBUOjc;fB1Gzw>c;-j>cM+3pgo?{7zaGJnG0En{1ASv zF+u17!Y2g)TZttR@dY>kX)vS!;P{~=0HhKNCgKO7WWkqUQX-#Nj0v*PaTfER5*W4h z2yM;?I1}M>sZrf15iLk2m%cVO5k3hxzn6&S#8BNYT&`xa8DQlPHm#`6tzEPI0&RXG zo=Fu6fHTNW#JW#+ji1-0ke$e$TJVvV$P-Vs*U}&>5#OwQx4DOT74cdC;IiJGu#Jd@ zv_HakI#D-D`0-;8^GeHpHDrI|)$HRhw4jmDbn-7Psq01w^R9>$DM%x~gjq>x96a-K z^JF1N-Y;AR_dP+9Ml{gYje0K7FxH)HI{+Pkk-!%YYwp<;2OUD)c{mZXq;!`NnW+`1 zZj`&WMUT}n0}(vgQ&nykVZh|;Cd`8+lykoe2wfIT)14&*6G?TU&(e5X*<5)ss_GJ@ zk<mUEXZTZ-cL^#Zg*z`4J2%DLdmnrq!JT&`)S)LvzoO5>Gx{gsoiof2B`Q8fGQ zZ@eOgYL6Y`abcA?u=y}KlPuN01^XX%bfF=cUHSb=wO;^MX0u0PHr?^PukrUp3x_*D zV@gvD@G{c@k5|x4eNvq}1Y}Zvmo)w=jxEfcXXmA|3OScDO`MYN4P80NdY6!cXFys- gB^XO%&m27a1G(0lI%A&gl>h($07*qoM6N<$f}=f{e*gdg literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_car_status_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_car_status_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..9468e56e5cad2e81f4cea9497c1d8fc7c43010cc GIT binary patch literal 790 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_system_resource_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-mdpi/rviz_fmd_tab_system_resource_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..77eacff63f4b7cc1cec4d8e0d954400dc75ef20f GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}EX7WqAsj$Z z!;#Vfl#BIraSX|5d^ga7$cUrzFaiZV)ZFf6AcX%Dw+q*?V&1Ce9<_w;TnX&X^N#qh*7+H-2X-5sA_}B6XL;(sunK_uQ9@w~GDV=Far&fBWyHra@)G zAV~<)5?9GrZ=ZK@>WU}z=Cznu?_T3`dhXE`Tfba; zGBr?JgTe~DWM4frunzA literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_camera.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..29feb457f0cd3fc5970e6e2a6c1f8ea5ed406f30 GIT binary patch literal 3882 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_car_chassis.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_car_chassis.png new file mode 100644 index 0000000000000000000000000000000000000000..775a7ebc51cddabd326d05416b3d5d8a75fad262 GIT binary patch literal 2483 zcmbW3dpOhWAIF))=9H~M$zdDGGejj!rP0J5p=5K&xt@}k8p>f<8b&$xR8KkP5Vpt4 z`LIZG_G2cjXlZ3REN4R~zi-#?@89A2-Pd*BpZjy&|9n25>wUf6chXr08wFWaSurs& z1)MF`35Z?4HbfdY%UzkdKoAdcvau3l_Nz?;iDZZs&KUwMGUO6XOiacThqZJL&z|L- z2p%)vow%sgkj`((9WBGn2WWX4VRRg&mZT)E&GzOz>9&-YOAkwQZD0ytAgytUL(BW_ zzPoJjBTP40XBYYgi&=SUH3WflQc5gafp-r^AB{M;#2T(mW1rgdP%+EeslTeZ{}c@N zxGB&Y>%eZGsJ5(Rxvz};Wp~-|Lfm1Z$hwj1lYI=WcpkQ&4~4z=G$3;ECZR`+vYnb^ zRgKk`4}th#Fk%uQ&>kqXg+dXNmXtJt z!ztqb`Dswwu3Y)sU}KBQf@$p}F|&Ac=%%=|vMZ)Ekj)EN@+kVw3mB_U)^$&ny6$&l z$K)+W5DJ;8%#DqO2?+>L3YTa8)1I^u%y@p%!pp;yb{&3rK1l7srD+QTW?k4V{qM`g z+w;TF)&ipF^0C0j`Hs4ij_BSJk-}C)l%cft?$O$`H&VCPEaDU(d`K_0d27(i{-DfZ zI92NmB0;ZyrX|M8g6u(jEa+m@M$eKwilmfah#3B&((Avcb6$+ zC}A;x5aK&#>3p3c=EDuj2PWel6Hf)9pUp_@bV4WOKN#wxO3Y2JLe4M*k>y|rc`6Gq713Z zati=iiD)iwQ3VxQ#Yp(ba(StSfC^r^@_A);nRlXDS627MQ39d zSc$d zRQRMNk;&*N^U5pwr9u}yg3ob!cL>ME3|k6^;2qy)M9M+W$9Dob~%No>+YUG5XyXCuHj^Baqs9!kRGacpda7}j5MD1|Yv_R&!&_z5B z(57efk42767WO!}DD2I;!A9v-`J}po1flP-*4tW|yinL8$swW)PL;4Y!MyK>^p2^C zDdY_o%ytok6n#w-xmNLUa(oexGAPv=5pXr51z08^tnrw%37`7-FA%Wmf)mc&qiwAG zZq3it_tWvQGB$=sV%0%2TibXY@9pL1edE{6G=j!HZ3Ol;#F;%ZK!MdJfLUb;aGvwl z*%Fz5h6p*gwe8lZ63k!iM?dEa)u4|u^!ga#TJp@vbfGQdJSMx*IIT<7wxDs~qnmY8+ZqU=g z1{*}aBmf=@c^<`1hb3sv7Uaa9F42T$9MV*LAddb>Sv-3#3Ai`9{x?7YSj#6Jd`PI3 zVsW+-ehq$e?%i^@%%o{h;U0V^qh{)OU zR?TWM`;j;8ca*;_K$0EA_l_y)KAS--ZEU6<5`i-Hc`S`nC6s$Ja;epk_ok69_PIH+ z3xW^rs=X>|7eu8gUy)qW^>#EIAPh}3v!S!J?fp%q!Nil{&d6HXBG#m=qIPp$%tiS% zckOp;9RDzWye2wtq|h~MGpl5(c>+c)d^lcSTcLi^n-9G~J{#T**cr_iCYk3=wDm*B zG#e5>f{>)GOl$2nLvz4E-93UkU$&*#wpaqDd4MJOxVE^gnm#()*wf=?Vc!$+Ec$ne z*Y$zlU%2^O#E6=Lxt()qlp#6#gDUcjUPj6M`_-mV`jya~kYgh2;4YKw{ti*tmIJ%b z&a-M_MN#oRyS-hngGO{ev|~POKYQMC*IUi)gmm~$`~I~g@^?|aLo(O*qmJnxfbovm zn?XyT-GKteYK7IVF`quA#uIZmdOQ&rxTi&+@>Qq)asv7aK9t(C`T;2IVSsvv1i4B* z9x&yhNP73csc1~zx;5he@jyUg3$ItDaA-D}NgQk^Wzg=-OM&l`a`B`WQ7KQJe}4X5SCR_W>jL U&Iw}@@bMDESvz2vRz7k61mkLa3IG5A literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_hd_map_version.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_hd_map_version.png new file mode 100644 index 0000000000000000000000000000000000000000..d134bf21bd600c9bff59340c16cab2de0fbc87d9 GIT binary patch literal 3961 zcmV-<4~FoGP)Px#1am@3R0s$N2z&@+hyVZ&BS}O-RCt`_oO^6k*PX{d^SrjlW5zZf{2Dxrd6h>3 zlKvk9`O}3j2?l`|3UO=Pb;xDp`>43yI|Xyfx*0W4q>)qg8QtY><6_{s{$T#)NHy{t&bLg}l0I zciQu7YFiZsb3GbMqOiF;T``QekG%vixNcQd|LuoOUNk!0ZI5JJ71XPs{%6-SB=BhKZ#ybg7Q5mzO9O^n zLq_)rnjAvDpdxcfMPgh6kGB4{!^Q%uA~TpKGErdFtZG93kYaOCLH%1d8ji)gHif{) ziM*gDkkNfsQ#C;a2?RjI5s$b9OKH4;))obYW^klRS+NH5p5w|7 zHcg0mB8S@#W^}K;st(ntO5quWA*oOeAeeJaRGC=N^C`N)-)CjO=^)dT&S9;~stN(r zhp+AY2mpf~p~^^w_aN54zL<42s#Qsopa`^j4TfAp#u-UARrvO`28a5o(Cyf@ufzaV zCYIg!0QKOZtPGqkrenqqYaInzQ&vbwlO+5niUe0+fkJXN(`*Myzh}Z7|D#Corg2J-)gGPnklQzqBnV^Wx}Kv^n6NA z_q&P>JT8u>)n3dKY3j<1KzLxvJ|%-|5r@2$e8Rz$idP?sgNcXfsu1ASRlD(x_!T8l z9)S{~)L0StgJP4A3rU63sS$qb$=4JmF+BnyqQ*jh8-~$`TkO>8HE8vkX_>OsRIMn9 z=@U2sU*0fco^IL`NEdpeK&QKHS}L@fs;RfXNs+2#+9YO7;G=>PDwOc*s@)W+N(i~f zGbVDz5eN@dI30%~;nh{US#7S(*raD9fm*$WyyCoRI}`~HYaR9WH(68c;9jpMEh1+m zfe+A9$%of50gc*wH zG*3wqB}9`6#Dr1Wwpq0-={ge5GDoL zwR#N&Wk${p{PCfY81cfD!P%6|`VO}${~@eImp5yJYDyLpTvSY@=wWyB@2DIZRJMPU zn$v?$MZI$(ObKHuwY9kB{X6d9Vtiem%+wed5C5dXM8S4@>nUpbZlpg>5(q|tA<;!+ z(;Hbm;T1OyED_5v&H65`!sleE@py+9+=1|q%NP5%^xB)?wA4h2Y^A50=UY!IszxbJ z{P@CqSu<≻QHsWlTZ|?OQ%>3x7UfdU7 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 T_}m9H00000NkvXXu0mjf7&)t; literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_ipc.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_ipc.png new file mode 100644 index 0000000000000000000000000000000000000000..759b4a590338ae85d41a56300aafd9bb7800c0d7 GIT binary patch literal 884 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VJUX<4B-HR z8jh3>1_ow1PZ!6KiaBrZ?$5gwAkqFXx!J)bDQnrEnJcS0XU@=i%BH+a?d>!l561|m zD(1KT$2O(9SIzr5ssC4=Ik3TsHAwf><99C~E`Fc5?q^Ej-8=8&w}oDQ{qOwlQp-DQZm*ep z$Tp!uZX+&Jv0B9vI$e3*)kSvK&i>u^KGE;#+ihPTU433xcH>Kx+Mg}Iw(fa$;_Zv? z4F^lDCf8-2I~`mp{rxm6pZV+H%IvS_YmR1bi?iyiKJdUM$q~1;2W!IQ?jM(90tP4+ zknm~Md;9u&>1vJ{t9~xE)CVeDc8dQ6&cFeCV@CPT6;W4?-@Sa;{r=&cZR^W(ub(^h z`1k2;SygxcS8ms9pIth8Yw!+sp7Yk&gBW{Yfa4_OoWtS&mF&2qhoz}9vi2QL4>9~W zwMzc>>7+M`39Fth|5RrEO~4?uE|CjwgzK#WMtI)4@3s8JSDzmWoB#V}V)2{e|DPY7 z{{>6|x!3$2$r*3re?n_ Rtm^~CvZt$`%Q~loCII_tJq-W= literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_ipc_error.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_ipc_error.png new file mode 100644 index 0000000000000000000000000000000000000000..774bd4c36648491d092fe8cce9a663bd8a4695fb GIT binary patch literal 875 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VJUX<4B-HR z8jh3>1_owPPZ!6KiaBrZZp?e+Aklg;@!&>>g}2szV$xW$=;ZI~nwMJ7I;(^#P1xA6 z?Pk$;E}iJICB?5cKD+KRzu;iY&Chw|;SKe_48JiLq%t)+CPD zoNIVkd9(?)(OB#&**E#$rn~pRw#5`5tgf@Wu75jsKl7aP;kxp_c_iYNJM`s7Pv0f? zbw2l>z1DRX|9({TdB`I%FI)$Q`$3jOT=D&E_Duj7hS(k^J+>@9R~M~(9iPn4 z82nUx8O}InYD~EJbRjT8QGpn2uHyRXlC{(0GZ$vE@b!I5kAxRy-$c%JlVKLa#rw;sRV>Q&c& zKUg}Y=wEx-K>p4pg4C_Q~(-Px#1am@3R0s$N2z&@+hyVZ)aY;l$RCt{2olS4zNE65ZhR6}oaGE(io9q?D7mSpX zPm%HkBe+hodrD5Mh(IDd2WZA2erb1A^^09UX{4R(5F5MyT~%Gx)fp5q7R4b4fc<23 zZGM*Rf8*1K!nn3fy|zkQ+(V^L*GZQ>A6sPDG(J#0D3BvUI|2n5dl3DN-qSWF-BaN z|yCEfAkBl?kwi zB#bJ&yRngrBwB&^a;b)9Eo|{Bl!!$Voj`oKR4NQRyv8aK3MDFm_?Wd+7;bnIN@OC5 zMj+DSP+{=k#r29vBvA-N{Gmtm!27~b-dS*gNIvK+A8nTpx9<|aTR9>sln6u;R3K6h zSKTSUh?f5Z7D+IHh%MtrFzXBz$q6r%ClEv+65>#+5eIz}iFbjBSPr!s(IFv}5F#1* zyx8n%$?vDCIHF{AKVO@ET3j4NV5bha8U8*z?sf_SADS8nIySY`X(Ft>8 z0}yT>OR8f)Tp+1FnAB-ndJWZ7A;qyEDv%T(h%}v!WxHtd0JNVR)g>np!c9$WOo$02 zwFkkCk7L=&$F7Y(X50N_b?x-bJwIFSu`$Noo0Q1q1XBF|AJ^}ad;nY|c{E!I-_Mv^ zB&*`@f4P2F84-bSe)*Wi5ltfm214W*I9Ir&?w?&CLl8$yt%q0%;Ks5p0}I?gt3diJ zj^2u*M5uBOPLXhr8{SMhmrps_M3Vqq6o-8N`1*5?mtTDr2XKs=$~hBXJQvxHE9=gk zlw=bKTM0lUt5ri1MhtM4rpzB-e{%dDfXT&TbFCr}#}HhpCPHsAi)}2C1;XY(Mj#H! zQf>3!<;K>R7`L)2nFx4pOdarjaJ>hPc!VqvmM;@n9NbpJB=Dq~HG6GL9q8R~Y>VKq zh9rS-h$FN~=Xw!9n68Xo`iLbg5~>xsqFkI~_A=eleM7YxUqtZ`ZpH1n$aeF`*R81Q z^>H(u6~Cvxg)-Xxwcg`}^u11#hjW(Q*@C3(T2byj#(gMW8+@l&w?(*TCxL{%X$tel z*Po}EsndXpQ5a>s9uJYANL*blCl@Q*|Hu#cbouD<^Z97I3{cWwT&mCfAHecaorMw5 zjk1Bm9kJ%*a`~!uSl!Rp^T$`x3f`%Rv~#T6IZ?>5uiSUsVO$V%-?n=t#<+W1KHR>? zYBkC{7ujx%yEmWL+~eh!uRY?_G2^-mEo}L{!*6Mhl z-C__1>p}W_2fB)5jJvnz`?M@Z6PJTvQ5^C*i+K9^J81$uT9R+q<%S$<9lxLTJz853 zvi0ydJ9lxbLMJL?gS=$y;XV7bBK9!|)wPOH9GGm@Dvqz1IHp`b)(HA>+ci$z?MYeN}m0%~}MuGV5r0tA~LmY}oB*zgYV7IX%qQ@V2vmPyk5=V#z z2$DFvTY~Y3v=wPi6cXJl=g(SVHP8a+{}897xVMlkL>X>ofW?U$nSTz#c9?)x>_Rl zeOxWQaS}+Ab*r}^gyz%I*2BiSd9h%UXN21_*{nl}#icAovb2XXX6vj-WspugI{@pm6(BTN zm_NSe5(&Gm8FphuY!aPSwQf{Fv>{V!!cD_K@MS%s$eN!p-`S;T&|u2Yxt|}W6}db` z*oPo41$!1qYi&~lbqB(5(0E^-tcRF5i1&XQ+-v9i6LPm)t82?=&7TGFaRW-5TPiI3 zL1L>ks3+B;% zyLB_TkFu5q>tTBUQ0T>r<-_fJ*G)w8$Jd{2E?|g!L=#PisJfotc=BF$B!T#93KttG z_46I$?ybu#T9vq~Tz-v25C9-?`j;tfx-x$Wxz=38X**}-|C99NTXPDD)qS}p?iR&i z{Y4N!a8bAjg0fc~I0?kL8-#R#UJ)00*_2gBq7ARRx>_euT!$6}M{|l(9MXBgz-WTx zVukRr#of9H%34I2Y}T=q%2d%5M}%Tg9P+CsS0*GO+`bsfTBLBDNu}@+M*zo3+_X$` z;mUQ1HEn;tc1Zb%kYxKG`BjWLkUB!dJ)GAh?<+ZWQMB&^EI_C@A{0?tkeF81 zSYxe-ms#uRLmWZDSs;|M=oxfXKujpriU z&7PKP;5g&Q-jySzU5zQD7NmsKA^`iUT;iqosgUKPZPb0S`b5y3kOPNu;E}UHk~(E6 z0ytfwq<1zZhFTDhbUo}+>USvis6agOTx2^^{4m*QUc)1Y@qfF~PxUuVPV7!yP)E&h?mV);3)nm3kW$wDP|T*v6SLYi`MUCC1Vib;Qr%70Om zzCoSDM}T|h4qeGoVuw0L$)zT`lBL87$KkXiKoe3*KcOrl8BLrmi0evL>c{;mN)u!k zOXCnFT3nm;m5NEv0-@}|);L6o4X!b%`$3dmLWw`h+X~JCc`mY@#vw{y;7Ve%SP+dv zl-O_{lNX!Wz6eC)5Ty^yo|bGL%@=`C@*(PGDShB_Tk|&;%QX&BH%o~I$K~CsvCq1S zrEZoI8j5>WRkZs}Ae1;n-7F+J83AV~WARLrh*OaePAnIl*sX@wpG<3SGUB%L1T}cX|W43Dr zLP;8`6;XUb&73#&JZ{W`P!4@jE24OVVm(?Ki8l&_;%2E8QGCEzcU48BKq$dFpjJeY zhg#O&cBDjWsfk(e?%R3*4gy)-&)1Z^2%xT( z(w#kCeo;Q3<)f{O2jtYWfsz*i)YVeD!7)}`cgZ>l(G*a2ufqBO!1cYrolNqw=#xO<~~9?*D1sfQYO03a_akpx05WCS!GQL14X69m`F zK<=za4cGx_JfeJoCGG%EcOJ4pSORuHMWO)U69*=nHOJw!8Tm#|%pYHWvNW(}FpEl> zaEgO#46!w@UOwEuv-~1m*%~Bl;sDbM>j~EG?i{OHz;X3LMKT<;;}HNVlAgf1S_MEnEg%wulu7M@C~~AY7Q_Vt zr1@YJUQ!(cUIYU0KcFlhZAZJmSjq_$ITpns|IGiPHF>+l;{@qVAb`Lu0;n~Km_EF3 zuL1#hb0lX}B+()9p0j+k9g1^V00s&McDI+QjAd2@FTp)n3NPs9Fbl(`tJBvaffcyi` z^3nD%{%;q|d03K!~;9S@i%L_;aFTwz#5(f~OKmbo7F+}zN?y1$N`T5wk0XV8g(Qq z5JQYYxuh(dGPd2v_mkC^0UxUnzLA(aRYmdw0gfWc_CNCI2h2c7)t#!M7XmS)U`={Y z53EL+UJ1mYLJ2_6gkk8VKnyAr5A;+RhF%NAphDrLPccRtgAj;8g+gOU!Y~XAVyR}xq9`+Xfs`4#3uY3UF_zARLIqNRsrMQ}@-fRtTi|%!y^7*H zDi90$e@VG2ScM?Z{{hDM9A^P<*M0y1002ovPDHLkV1gA|Rrvq_ literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_launcher_eagle.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_icon_launcher_eagle.png new file mode 100644 index 0000000000000000000000000000000000000000..5042b598e2880c6ca3c882a3b4f334f82ea4d816 GIT binary patch literal 5325 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 Y57f$4ZDGZqGXMYp07*qoM6N<$f>z|szW@LL literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_fault_code_select.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_fault_code_select.png new file mode 100644 index 0000000000000000000000000000000000000000..57aa3881df49a47f9967d6228a52a13e2478ab98 GIT binary patch literal 2127 zcmV-V2(b5wP)Px#1am@3R0s$N2z&@+hyVZw`AI}URCt{2T}yJSIuJb~cLS{G;<4F6DDT90Cn;wO zcx>K^SV62eh{Tm~w~#RU#NkwB5F10IgQWf=kO}H$XaPV<{DnXj<)RWj0Ejo7x{tDt za?Kft)B*tCWJutcQU(E>x^vm4ltIan7W?<%8f`|*L88BWmb;W96e%xWH$$5Z82}(q z>7_HB2#HW1hDzRWO?7=6M1ftZJ>^e|ivj>{{LB!*DfH#7%EPy8eP|gdBqKx($mGNL zYdED)NQu8OWiXL+51B%|g`XKtNVu=1BZRo=6zg^ls;-HwX2^z(knFG#k{LEaGQ&nl zX4nYH3>zVtf%Ik7hoQm^RZxqSGQMOQcLgPjC=kqJV6(R#2+7JF{>qT(g}{GP!*nDh zXYcDvL!vh$mIbhpkrV=VV_yJ({7M@>-S97`Ky0%k9|+*|M}FZ*>RM{kL6J!Fd10tA zuc+_n096LT{uf8j1;z?e2agyist?1%^>acf`N%koKqdCgAg7;S&Sj9~hx#y7w&^-9 zlSm&pnm)lH^0uh4j}}v}o1v9AX1~%9s3gA0s+$2o8e+)h1US~s5L})pzO4E%RKA&s z5;&?sXak8w!j?rDL!RW$MFE^_KCGCexI8$agE*~ofDL(dCNO^)0 zQXht@ZicpQhPL}Cd(L}m-duMsTkCOwG#(on0yt4Jg_0)-A!-A^>U}_K6HcVq$P46A zu#wT7OZI^Z;c^N>2(=E&FC1rQihAO@kFw{MueG1b{rg}gW0a2?e1U3hc~&QcU0VgJ z{rhmWl49I+cP?9MvtRS6u}n~M`j8Q-WFblRyS92$m9Z(U%8C?Z05O&<#^__8(CLi^NPOr% z%AVYx)a*?~1`vV};znHyaGd=_mSP`QaNz|B-uW4$vMS1^-kr^QpA9-Eds;T*#O^M+i`3A1tQs)W5`>Ns|FH2kIG8 zawP+x2??h5a?Zg!2vp0#w4!L}myQeH|W3cB)neI=vJ}SAA z0Yoqi^+vh)1dgOkl?JZ1C1)~#$d>wIViu4>*+aRL0Yr?4bmy|wF>})}91l?cyX+M% zq};jp)p>I#0|=;PJp)&*>L+g6CZcLt&%jlyvMCKIlI+$%mV^NATGgwOk}V;C`v(3> zq-0G9@cT%~o)F-7k@6!50qm}rS0W`lLh8d%nR&tHcdmo|lS#?S4k7J|Nnt&EoPue8 ztZAp*wLd>%%AFf=cW%p6`)B8wHh)>eN5>mK4Vk&t- z?_H~AR2sx^hnODmYv7f);A#}365>mShEFvWmw`g19ofiOBbG7} zV_ABqpG6~NDH-~=WVMrO!$FX3bd=u9HJbGBhYffXLT+DsRjabHz)+2uV&x@vrxmd2VX& zh@PE7_|Wut1#7KAeeN=#tB|2Fxp8tMPS8!8E%$DQHb_Yu^};Wg0d;M2;R4r~M5^~* z3E)JnBGK%Sxjyo&+e4yXZAr=Gy;lI}WlQ335JI??7~W+Azpy)MFz2G{AjcwuR2Dq1 z@i0~%`wta8jM@bA@>%Z4!4|0Y%^*dA9qSSODKo>Pnpb`SaCm!YvVtA$hIstqhmhe3 z&hIuwl!6_fsU3<8u3S6BwH8C7kJ1f@UA`s=`Bs}X@Qg)Iy8nqT717!=&5;tNV}e9K zy?)&ct*crc!=jHGpc;cUdI?i;>R25UT-Wg!pu7g+4X5Z0%#v0$73gt$Y#|PyjV=pg zq>?iv=iJw4VDllxl!LsUS~Utbik10mB$mQ8U=<2n1XO!B#9V=8SnTTj~*a z{Mw}WWM7V2*3&&nG6prkwnolUbs~CJu`$aUsPCEfACr?PAztlelWe0+humJDswjO< zz^=V*#=!{#4koAR&`q7dbcrw(Oq8zBZ<$_SRiRi)kt zA+RPfT5)i-Z#F_Kc!n5XB8=Z{aKT89hDnmL;nsLd>Hi=~3o2^dYBm4>002ovPDHLk FV1lYI@`V5Z literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_fault_code_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_fault_code_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..3ddc2367b605017e0f0fdc42ea24b73ce80e3261 GIT binary patch literal 1988 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 i4>~c%XOP{!pE0@RXZz18bg=d#Wzp$PzpTPw@} literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_system_resource_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xhdpi/rviz_fmd_tab_system_resource_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..1e38a0d0e1fcbd311d666691d8087f1f9aba2205 GIT binary patch literal 674 zcmeAS@N?(olHy`uVBq!ia0vp^DImw-*oBrlncV_z3)-^fLn3E(KbC2BEuKxW`>xE;V zmRW1K?TVNCukl! z-u-TZ+ws#ki};dyR-Fsv6PNZs{reT`x&FV3N=Z?te|eR=3I5&UG&lXkw3~)UEROCv z^G4~BMXqvx?m#=Q5c4(gXGQ0Bi@A}MS>Cbf?9osL> z<^E=Q=-9SrZv~##pJ|u1Uvcfmvy!Ek|K8hFxbgm*Z&|&$7a#u9TfF}8CiibEO}Duo zb>#ZW+vj>yDCPXZ)U_MW-V|E0f2+wI%bInj+bpA}ujyZ^o)P}sFC%$&P|cLm)Q*oX z-ba4eEXz;G5&mamv9ABPd{0#J><@1i>%2`7kZ3+2!IJj2zv`s+np^p0C-zCCyiJ)f zPun=%F=kT&4^uKk#W4i~wnjsa9!H?i=NFu-)Kbze{@A84&OvAd zx$w+A;gIwHA{O5-t2XWqzp?V`yHEGiUT!Qu*#vbxn)1cs>eu)S+kk-pb<7$2v$r>2 Uu+qNM2ux=Tp00i_>zopr0C&_Xx&QzG literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_camera.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..7e240c20c34be84df325e9cd1fbeb982f412920a GIT binary patch literal 13940 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_car_chassis.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_car_chassis.png new file mode 100644 index 0000000000000000000000000000000000000000..bdb3e532bc96d667fa43e78eef707ae2124957de GIT binary patch literal 4976 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 mZOwkL|FE_qnWY{ydHfJ>{helF{r5}E)a*FpCH literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_ipc_error.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_ipc_error.png new file mode 100644 index 0000000000000000000000000000000000000000..28564a71e7750cec4c93c0a645e1e0f55e91839b GIT binary patch literal 1447 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4mO}jWo=(6kYX$ja(7}_cTVOdki$~!<7VFi12)G`5wY2?Gd9{80{`qHTr__q7*zsqmmZogFid)Mu}>$$hrwcn}8 zob_J(`t{tEzQ=!6)&1Kz%ZKgnA^UaLzt7j#cdvf@;k<7J?>-?WqTNS^udXcRKX-rc z>V0$O>U!OO`}+9h%Q;4K`F0hqcwKeodO+#j4dMnfeZ+p(et!BUCoGBokKQu#*=Jwg zP-gh`VX6Rep@!kA!1ZCPk8gAb#x=>{&&p-xx8GL15oD;nl6#v=Okkl@HG@Mu=~0Pr zlw0ht2bUjyc#IS|&j0;t!$ZF#N6o8UKPO1@%iI0={`1qPfUL_K^UP*n-Z<~VP}&(403kH1=4zcn{Hf%lIc#lHB@{GfT2?(XI^ QS5V}7y85}Sb4q9e0CtNQng9R* literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_laser_radar.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_laser_radar.png new file mode 100644 index 0000000000000000000000000000000000000000..96e954f0ca130a90e77ee8f4cb35226af96f95ad GIT binary patch literal 16561 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_launcher_eagle.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_launcher_eagle.png new file mode 100644 index 0000000000000000000000000000000000000000..3c721ff54ddb08f0fc699d25b0f85629c3c812e4 GIT binary patch literal 11082 zcmV-QE49>#P)Px#1am@3R0s$N2z&@+hyVZ}07*naRCt`VeM_%p$(7a@`<$CMGpn+yGM!luyI!_& z6}ACCs;#yxWm0J;ym4>$omNGmaX`v)nbgFTZTq zt45sSx6eVGzUaYxj#Or!Ie@11VX*!bnmXoUh>0|hykiYFnj9rMSonF#WMd-pNA=kV zJB6!IljqUDXxDiX#r#MpU_QD@AQ=(Ukt195;L#FjwDw&Q`pCc{5s9{7)eUFoF^Q4e z9SUhsVmJzOu|k~i3>KBCjO%jj#>b z%S8ghT1*)#+h8451y&UWm5acI2qu`Bb~el%%Dk(7K~fZWI+Ha z_I;c@P^)5j@%2&WiJ2V;f*Z>MuyHzjk1M){r;O|Q2wpN6JB&-lLmtZKT?kz9!nMp({U-^R_|0t&`N%u~n|M38nE?Gfz%m0Zfr7z{N)I!`mxX?}^J3Ui{PsU;XkK zeE!8PKK}TGNw?6Vd`Ln4rw~R2*v*1h-#Ec{zkiIs{NX+P?B)A7IZ4VGdKzV(I=N&; zm);19My4o0_nWxX$_6nAz$kz4k8J@Mn#&CI-o)N3gbSc9G)Ej^JKRd<3BcJy;2(bT z5dYv0&f?KW3N}+gnaJk$C{sT>9NCu$uq?nl3y$vzUVimH{>8t(fw!)mU|Cl9V0n%B zcu;<`SEkTx{U(Pn?@hR%;Ps3WGqMkt&lB#nI%48U=M|@3`)7ZA4qyAN0~~BiNM3L= z!5;Tre*oprv*7C6JN&!?K&x{FfJY;X;I1;mz8V%rVce3ZQ_Zn_rJr2>y2t&uiU>U1IBd!Kg>VcwpJ$dxu16+9M0LS-sc<=ptFyrCRRzD66J?`?w z4X#|8LKGn#IE0*-?vGuq7>juf9o86^7G?i6w*qa@M%3<3h>k?F5}AM zkKxAk5Ao`+t^o_FUp=*iCS%*-oaiv8(f#0XQ3cS-)-opfjCPGw2-8%W2lwMi>Na_C zwA05gPXN%?p!=1ZKPfQ%W;iXQ>!kpfE^dY+qD!Umv6gufU^A8K!P5laH6j8?M!}G} z?#4zYBG?+$nFjJmcP?o($1IFCmX}6@{t+is^+JBu#-mB?`TSWYY2<;X4QTw*9f>aw<+;P$88=b;^u*mvsr&RW22mp zY{Ky5F7CbYdN+qChT z`D(_MI1do$O?M#fkQnR^myq+NCvJsY*Le-4k#J%ro9fBznI@nqdY|&J;(1!mHktW- z5S&y62R?}IVQx9lQjK|81oJ#&o|i1KC>ITF1BI{-dO4Ts!8YEBjtw4mIiu8tBBDCu zGQneuRKoUW&~`hqw4sMT?9Du}?Tx~3P=3Y@l*~4_2yikBLc|i9LqyRGaP`_-Z@i1Q z-h3Cb2$ogPSLJ5;d0#n$#%0?yhBF|LIa|8$&@r0rC+uPo3v?J^M}JvWG}W!0mR$c80V{=UFSCqj0=7zV1`5)dNqm zgz(Axln=Op_xw5$nt2+_Wgm_)UPouxakIg+4+^a#D^Y@-^IC@@K{kf=v6g}heI?w? z%cQKZffoXUn*nCQj`nn?Nm=aHD_ukw8P0AV%f*z#xdL}Nf(oXoV44bKSuii$9^uF` zR~<1m&*S*kk{Bx)B^N%OCqk_Y;ZV*NO?m1-MC~}Vo!m_z?!hB3OA6iX1ScoJ&D#q; zyd(JV!x{HafMr?Wo9Y5ts({a*E4XlBgGV0T;_P7+c$wN@W9#(T-y&!?-X%ViL0ayq z-H*o5r=NKok3V@4*WbT|U;g4PH;7KQ)E=OdBgQhZDQ=3%u-L%ndF-{f?+D(wI^*Vtb5-6_ z1yz-~kb+kft;!Z)H!rw*w8Q(?@8Pwxck$da=W*%c8BCiJcB=HL2xY+>Lt^GBk47Q$ zG?k7|!$JGvoHR{-n*ahX$lm3+?Y%Yj8YV#7-I8S%Ml`aakTE*Y|5aav?Ybp zKQF+ww`aU{ZLX=z4~v_yw6m+Vm#jRL<3 z$MR8`C1rQs&wUQcGR~jyXCf?A#L%iAJh{qa!01?abP4U*k}N|+%!Ac~-nus9>a`tq z^SYvzWwCZ+XjadyeJ{H6qt{yEVcr36zjK7+;~AfQ@i818ZW^vm{4yU=dE?BJ;iSaE z9NoGQ?d|jR7$|!j?Iy(wy+!DUq|W(Mvte|!5Vj;E^2q`Vr+=PTg=OhV0Jm;0xO#1e zliiY(y5_ZN97|BFj>Dqkb)!$eFD-h#fBgurygK2<&s=OT5N*q(#D_+owanAL%?4Xy zii|^V*B;Dk`Vj5<*hXbCLiLkhS4d_+VMvMM86J!V55>wP4^h#q8jiw$&Dr`a`hy!Y zPIgPn;gW|gq*N`}G@xSCV@DouxLSXq_O4#NjZc5-F>Izf>(>y?Vz5{wX~Hw`wl18> z@YIGzE8`_sSBIi-!}-0aCe+jkyS;?Eu|XYs2E>bCFK(1UQxnw^9fL*c?$WX>o+*T$?)43JQ&EBwT1P;XD)5T8vBvW_<0;WAJVf%ySOUbZ#LQ)|X*4G$-XL z?ddAc_(8XPgEPt z^HR49TM`CY*3!!?&xuaMh0TU|KDs->!^D0Zx+8?>gc~y;7NcyPsZSxW=kbIHuo-d9 zT)~tc6y2_~Vtb`F+)VT;9TY0p`N;et;7S2rf9-9&@!H!ygrsP&s#wiBsy;|w{-*hQ zghy9QQNc)r8Y6`w5q1%LbV7x2X6hxkwb`5J!l@*SU5 zr_9qp@~40A$_8PXuDNqEpz5l-@!gbX7H3*$xEMb4x_vs_`w4gnxX^EFaM}Q$eC7cE z^iM9~;-g!-IS;PTMTRroe$oU;r}#A^pmR=$7_n|-r4(Gbe29Pa$4}$i-}wOl<-fk& z?j4It^(7`LfMjm#-VrXzEo1^oeybBN5lq&Ub}CF9DZoK)5KL4AhEL~M80m%|`@a+uTdX)4%mHZ?CjIM`SQscqLvavna~BYUJ=8k02~tJm~KP|Q=J zrHkpX6kW}FeIG-pZ65JO@IU|RE^gmh@atUjQ8KaM! zKJ=f|HSc%7cNed}c^|hvoN;`-;NJasZ?uRs!RN5L5^l8&7-zh=4&|h^jmfUnEY_Vn zGyeU5cpXn%ImCO{@8js0-jz*f{SRfRDhj4)!gjmCW||Dw@9)NwoH5jE9${nDu=iv& zay)m$M3s21YKm=KnLbyEvq`rPoU$0P?a&_FaF{$&+eTvK1Gd7hsO4x&!)A$H-8Q@BZ#UuGydw~9q zV}wQJnvN`jghyALn9>&Xg(?PIJFnY-@kaOBM4VV?+asJuG4bE>i%VjN#Qk8P*d!^3 zXxjr$9+*9vGhQ8DI5V~wulgv~+-`a|kqp9S)aKlAg!1-2X-h2E&B^>FMP52xI_1qrj^dETQ)R>1IR z%F9oHbK<>a!FWs_-c=ulvogR*f8)C;Erc4s=KSQXEB1F4RK)AG5bC+ToIxW4(^mj% zyLCP5ipaY}AZ4vx8{ssR=FN#xCRZ?}NITwHDfTgzUpy!(fa^r1DScgSc6n-l8;}$p zq5|SlCx;5LI2*4gT6+(dcoLKwNJ?X{z!Dc>mYlqf_l4CXSlRCJ{mzmqzs@m=-KtY` z4E7N+3Yi$-K*Z>3%mwqZV7HrXZ<`v5^6CtU^ufv=7+kok(yTxm7xp;91v|^Qc$5-I zb&DqD_EzpjRuvQ1!9elNhNei~yJaOwQd$nb?yZCGi}EF~2~6Re50d8!Z2hB#Db7?7 zzKGzpUtY!QuUu`BXbIRT_rY&ZNbG0SK0tR>MrU%Sa^{J3%@y-fDu!$)BJ4idxlr4T z^Qf`ReI`pc%14S?UGfO(wjn?p3JK3Qs^6QUmaEj9n zfAe~`*!UZLxpB8x&t$RnMuX7K%@_=)*Xo6sK_jmgUz${p*Pp=|Ostxqssbo94>MV$ z_+e6{wpZV|IW+_E=afyTO&CgVK`~0iIn83c!5le0PD zK(Rs2m8h+2-9rLff+;sxqK_e-Gx~fd)RdIf9fk?#sU9lR%XL_#c`q zRn|)A9w}Dwy39g-VQ8PKRPTWELEht@$DXUQD3~?{+k^UaVbvl!obs2{5d_Q+-$$H` zo-TxYF;{R`be<$;p3=0TvOyDt9!Cb%qD4%ycuR|g*jg5I(TGawCSQBT>9scKyoh+B zLt0kN*Zz&ky=yk$DX+haS6mnP{WS-nL z15-7`a6ie5t>5u)^acd=)gjKJ+@GyL!`u1go5@y=SG%2)!@=)W_o3$%#%rl#L|f-$Tqh(mfy&bwyfM}5#B&l z7R>!RZFIIKD8ob-UG@d%aCLu;kJx!#sZ_9D!YL-2%$Ueel$CCei&GNUT|8;Ac6 zNB?#1vkewt5iwa?W`b9**|O5PbIo9970qAOZZo4p9 zAyKgp__mPo`>D4%BmKe0KL2_j1vhjrWpD5GTxT}+YTx@EkQ zQbp`~rL8y*Gc0Pv>4@{8nWy2RI9qx_a( z<|(oxK@qDsikRxf^|}V~LhZS6_>NZd;1qYw)kT*F zbO)z20lEhdl)3`&K0cI~|ArJV-b=GkRb=D_ad_H>m zP;#nLh+x|up+mYxjbZ?_=pA}t^jz`+vzdX1x8wf31@ls0A}Rt*)5f+8A>z5}kll)j z5CDC0M9(!eTU}T7BH|tl;Qqb2r*)*r0l!>crh-kG>fZL;%1OTaWx0mq!GU*^g7SQ~ zXf!qqLkZ)$B#n4IR;*4WQXMmN>gdkV+=Nh%&1M4;f4VSw-5}i3m>RFw&>FvsD^~Qz zyOxU<;OJ=Qp__!dZvsj2xO!VD_|yx};^}9f#5>pC$B%#T3yX4APk?-nmw!lXzjdGv zqo4gtSgcYzFOl$!H$@fVm7;aWaEZ*vu9NZ5#hW+pW1j09Vnu-S51p^i2kXu6Yl_{g z`2F8?VpmYGJ#zq&niIcy>t5n76HVXy?KC?!(}eB87SlALOaSIw-J{zm0>rOzMy6`$ zp0jTPF#rGx=t)FDR4(-Rt#wDCC-le-?6`a6k|G~g4tAm-mtwSy z<-+od?W;bfI9{m5cp3j{m*bR+h8M*xVN50Z2}i^T{iX5Ba7_Wb6T$!a>l;n@z(Wr| zgtO-kL`}xmbM zjl3**VbJxOyqZiT2z#UtL=P6nO|Z!if`yrbQivGi#BuOCL4k#T&!H_43`bOHK30{C zNcf#QJG}Dh?dn;j;1kb$9Md$J7cS1%h^Fs*9H?!iWoFasgS8J2*O;u`IyFOON4iyznW@TFv$jj`n@_ z_g@KlY<*8w^HQlVHJ#@+VY7$Y1czr2@tGH205D;m1wZ`Xzs9}$Gcyv0E|UvEiUfZ7 zVvt|(R>uiD40R<`$I2{hJ8n|lXoqf8edft>p%Pa@)-dDpKrCu!#5?6wb*%E`xAvgF zB3`}w_N^0q=YPF}lamFdOnBjMei~0b^#m3QxAuY-T1N4ByO_cF%U5bvY-tag3(g!K z;&=Yu@8RJK7ceh^SAKaNue@?Stf?b^WV9i4Iv@MC4MRzoyfr-(-inX`uJuv>^jPLIUKd;?dyxe{`q2mY8@p0=3n-X28$9vk6ZpbQU&7@pS0DmhzkV10{V!g|2Ok_IM+k)4xESe1 zD9^ZuFFb^E=g;E!=oq(d-8LC<+me*%mvWy2dH|F!fAe+MUmQ4tLTwn=ago|%!reA# zicg+BG9oALK{99!2f>*$8~pCqKaS@=`DmMLEx7&R9sJ<0eu#J9eGm8V-N$a8!@XE> z-CJ*gnKl#7o;`=Z^U@db%yZ9Tv)N*v1vhWp#b5llmvQq(Rxa_u{0el#H_{%Am*v8F zhkKJ!`Q~|E@Qa_li|_sAn>ac; zIh{T;+6U#=D(%5NfId1p^72(szWj~Xr7c_r0_=Z}mbatWW3$rDHj`0ghP*v_Fq~ea z{z@r$_~8S*^wJY}=9x!u_FPRXDt($mhMubcO&E2D>f~g`+t)tC559jD@4WqCoIea` z@^*eh1ws5aV8f!FF^UyH(pCqIH5)z%-?iNy*3Dy_oXp?} zRS+Q2!fXGbN3}3)4|jhgegi7jW7CF1Un>4b59 z*4DoP2Mf!3*>O=Q5!dctS_I1sy#L-^y!Y-M!pB=`f-$pVl|B)F8_r&wuj#8?{JmBt z(~jSs4_4TIT)H(VI1Veh`%#WjY}Xm5GRA}iPAjhSHZ6<93Y~lZu4wy~$L7R%hXd@M zr;KCX$N`90XKdVWZq7Yv$cMObxC0NoJ#-!QVQb?xK3p9=AE2;*G_*Uysz>mHvb70J`M&3>)N4HAwz z>2XB+h>t-?8sjo(CAnY`VucSp08U2jIjv(kk8Z~u;Hb;)>B7DNfh!DlZUI*iuJ`L< zGx>0ReSJ8)z#!13y(Lm6$e)D)}6S_6$nWv#)5FdT;i2d`gey`*9Je`>$gYq$^FZ2w^>TehF z`G~^VEk1#=$}*_q(1;@>~WTlfd5)cODyEFZEnE5tbQLWm(QyhhDvLNT&KeU zMp1_gW!Zximojagr{(svwZyNG+9`0Xu1^ie{f;t$71M|46jW5|CKsVRe`wBzOr1j5 zRANvzaZbsV(unP&M#oH;)@8k=d)G;pL3*kl?wAi#~d?0<{aZO+l$t$TKgN} zQisC*_E94=Z5|`=?Y$KeL`IkT3T$vWY$m+X7s7vN$UjR8$M7z4Xja_+1;H}!kPj~v Qa{vGU07*qoM6N<$f(pNR&;S4c literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_map_verson.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_icon_map_verson.png new file mode 100644 index 0000000000000000000000000000000000000000..13ddcfd8618d1ccf6ac563b6d7ad44197c5a334d GIT binary patch literal 4008 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_car_status_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_car_status_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..898061483b34a518c3ba05ee2182cceffc71b3bc GIT binary patch literal 2380 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_fault_code_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_fault_code_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..4b9c709371d73b7d4701ce6f68485f9305deac4e GIT binary patch literal 3043 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 literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_system_resource_select.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_system_resource_select.png new file mode 100644 index 0000000000000000000000000000000000000000..e3a1b8797734616f54463edf39ff5e428acb012f GIT binary patch literal 1137 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm4M^HB7Cr(}jKx9jP7LeL$-D$|Sc;uILpXq- zh9ji|$d&eVaSW-L^Y(6F*ewT!)`!*?c{eCM-h9)^@}0{K&;GkjcHP>ixKe%vEXq)v zdGd+Yg#Xpqs);#uYKoky8iB?4uT2uU|3$%*YgO>{qmtdXJc2l?YO7+_1c`Gy^Jl!f zZME-Z;{*jy$Eekpp1u5@Wc*>r<5{V@yKbEgI51CP?eDY=ixcnu&5EC~^n!NQ=RfI% z)i2!H4=ZV@SPNaRPZSj0B-^q2)_>>CIpHp?IgX`3dj9{ccIMJ4TvPLQmv>)1kbM01 z-2WEZ&n=z_f2!*X-YuEE>84he@VYJc?2ljco#^`d{@UQUkDV>ZEe z*O*2|I6pUavv)rqee2u}bD3?1=Wpg!vwH4|-nKC%_V@2=c4up&kFSlJblrNx!|zXz zJ(V(E^8C2#ihXitpL6}%BmHvoY^CcfU!U7$RyuWG-18Y9-ye;r`7m$u$6~qpyA6w( z3VJ2wS4or_mp#0={z*%De6m^EDWh|SkLUkb#(w{0n_TnmwUc@?xD?XExmRjgU5=Y+ zmULlt^4>`~$;Z-Njl*6seh4|eRHEyX$nl!Orc-?PcJKCD9=Gnvj`d%5E|1GwSNBB1 zJNkZW-fd3b$20T;BO}(;=RB7cWBH`pbx3OWX|I`~$KF&0`F*oG-ou}2e68_!LE93$ zy8m94tCsZ2hdg(St}0FH>+0WF6o2w$|M?!X=Nork|2Jdpr`Ai{7bfxDtNL{HM*qa6 zF&%QH;!TBTW@xR+OLbS$*!%a%jr5z#ZhTm0c>eB^#J#uouQB?#+ScPW(4#+fl4L&1 zR319?=IHjkMr=^fi4YLeS@*l{#@vtpmTi2{!xQ`Q1+Vv`X)!X_ zbZ%R{s)%^Ql?YeI!7bLp%8j8NNjT|)oBa~apZn#C_BB3^F-S(S21y;V?SzFNJ+kAx zt8+WFXP4yWoE?{b-P!l(X-NJ3Zf@^OKk^see-&zVSbwS2!TZx=6?F)k2Dkf0-j~O_ zQ=j!p67&{a3ox*7Td;4v&B`sDw?Cf#v`78l9_B-LH}$&LpK!lhc;rI;!UO9B2n8Op vEmiqnn2#kLLysw-<=4CR`5#W&_m9bLK}mzdgxq<+VuQic)z4*}Q$iB};t&Eq literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_system_resource_unselect.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable-xxhdpi/rviz_fmd_tab_system_resource_unselect.png new file mode 100644 index 0000000000000000000000000000000000000000..653c0d6bd04ea698c99479586e10f6904a5b3cc6 GIT binary patch literal 1111 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm4M^HB7Cr(}jKx9jP7LeL$-D$|Sc;uILpXq- zh9ji|$Yu0&aSW-L^Y-rDz}pTY4iB@P6iW{6np|=yRMk^mXRgXr75)f9&%w&pF55UBB-2c!z#6bMd{^$!XVg55GDVmL~n~ z@!{!P(v#QM9&|igc|I#m&fmXv?fyNFuN2kS|IScg-nDYYuZ{g1|77;e`h0I{M@Rl& z{q-h1S5lvyx0JpXRX;U|ulRkwUx@nBDfbQki|U%CyG{w3H_QLt^goG355L_#RQUQ% z?E%Zx<-&sBd4KCyeK>z*CGRzx8OGlaK3gNs|D@>fjFs%$&3f)`dOu^U9?NUpB{Wndj_mlnH&s#OuCW>b4pZ0m*wcqzj_V4|*s(1}k zpRaC)SjL^zSERyjCJD{A(6ilEwKj2W?WtKdtJYM$nz{2%!`aC7v-%?*Bt4$RUUbeb zHFJyJ1ikG?Ew5c-c$T+c<5nyGzsF6v$~IBDWhc(qOZNmnKD$&s|LL#o6MN$tFZ9pd z_~+pIgJ&zhZ{vEMV$j5^XD?Z!f7^}U#`JmYZL2kttG?CVtazh0&;P)7x$X06_kX>y zyO_H-dO@Lb%<^3v@mn2MX{SdxNlX)HQ*BA)OawE7Uq{4NXs&PT{dY9EsPT$-k*P|& z{iB2l{;g;Vk@TQ59rp@Xl}p*bPW;sDuh;9YqsZgUaaickh86>|nd{!oz3|t%>wWmU zd$G6nSNx2xzqguub$Hmj?;_KG`ma27>hFg6*OxmHZ_@$2FQ4mAznM)H*PW=Z47&S$ z)pyxaU^FRj{~cD&JL?w*>wK-cv>!b>+EkC^HoR}UTKAWA3!~(6G3Ra< QVBx^v>FVdQ&MBb@0RCI|HUIzs literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dialog.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dialog.xml new file mode 100644 index 0000000000..073268238c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dialog.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dockers_item_header.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dockers_item_header.xml new file mode 100644 index 0000000000..0ec9aaa96d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_dockers_item_header.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_fmd_dialog_btn.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_fmd_dialog_btn.xml new file mode 100644 index 0000000000..199b05f019 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_bg_fmd_dialog_btn.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_dialog_default_config_input_bg.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_dialog_default_config_input_bg.xml new file mode 100644 index 0000000000..fc93cfad8c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_dialog_default_config_input_bg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_select_dialog_default_config_button_bg.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_select_dialog_default_config_button_bg.xml new file mode 100644 index 0000000000..b19e3d5114 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_common_select_dialog_default_config_button_bg.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_close_flow_button.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_close_flow_button.xml new file mode 100644 index 0000000000..26d94e0fc1 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_close_flow_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_color_hint_button.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_color_hint_button.xml new file mode 100644 index 0000000000..c2db7690a7 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_color_hint_button.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_btn.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_btn.xml new file mode 100644 index 0000000000..199b05f019 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_btn.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details.xml new file mode 100644 index 0000000000..6335534c96 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details_item.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details_item.xml new file mode 100644 index 0000000000..84e903d3ab --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_dialog_fault_code_details_item.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_btn.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_btn.xml new file mode 100644 index 0000000000..2347dc3e56 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_btn.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_data_show_data.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_data_show_data.xml new file mode 100644 index 0000000000..49998a1556 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fm_data_show_data.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fmd_dialog_btn.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fmd_dialog_btn.xml new file mode 100644 index 0000000000..199b05f019 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_fmd_dialog_btn.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn.xml new file mode 100644 index 0000000000..c03996766d --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn_reconnect.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn_reconnect.xml new file mode 100644 index 0000000000..9dc087210a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_btn_reconnect.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_even.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_even.xml new file mode 100644 index 0000000000..cea175ac06 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_even.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_odd.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_odd.xml new file mode 100644 index 0000000000..1841e7506a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_dockers_odd.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_error.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_error.xml new file mode 100644 index 0000000000..8a9f05a659 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_error.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host.xml new file mode 100644 index 0000000000..802ff1b2a4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host_loading.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host_loading.xml new file mode 100644 index 0000000000..77e1eed8c0 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_host_loading.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_normal.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_normal.xml new file mode 100644 index 0000000000..1f4b041b53 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_item_normal.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_ros_host_view.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_ros_host_view.xml new file mode 100644 index 0000000000..802ff1b2a4 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_ros_host_view.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_show_config_value.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_show_config_value.xml new file mode 100644 index 0000000000..49994d8c22 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_bg_show_config_value.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_dialog_default_config_input_bg.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_dialog_default_config_input_bg.xml new file mode 100644 index 0000000000..fc93cfad8c --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_dialog_default_config_input_bg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_connect_failed.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_connect_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..8a466d9c8baeaa11381cf63e14a1e2cbb784cf21 GIT binary patch literal 2743 zcmc&$eN0=|6=y>kDXbtGz;2y{Xb}n$1VXf7z$EShG)uLaq@DOM6cW3PlCc;=jQO&? zE^8gY4pk7bqC#8}ZA>G^xO^3`aZ=)!GFk}B983)6!!S0$7(+0oHulbY&uIvm(myNt zzW44q=l;(5opbJyD^Jr-zUuvkw}*$vtEYaI_zt|kLf7t}z*Cmia>K*Jn|~_tm%lCf zk?HOTr#`j#vk{&k%u7Ag`^hCA|Cn7-S9=0qnf)o^-x}u?T6z6I--s^Dlx1qU;Jxb; zk5X$iLS%}fUhC3kKYIbCZU+jl@dcBP9VT0z&z0k*4zba9hMTY>7IJ4rl3%iYmf9ea z&n<9=rB*GWYR~cB$sEPjN4gmFty0X1kHwozT-gvpZlg=)lgsTV*k9&u^oDm%YLKz5 zh-y<@`Pn1Wfq*^% zs)@8r%YXEMDqmW8d;bmV1x~b~BgHl2M@Qw|;hn6h6>nVLmr)(IpU0DEtm3`PPgdHK z!LzWHPKa#(VrFEtC_R7j!8A>FoD{5C-+b8D(?3&nl4br}*Qe;;2LRE>McST{T>znK z7-W=tT_cp0JZ?&nin*+g2uOkTA2WX*&Kd>kd#I(2DY=n=tS)!9cUc+_w^%L@XWa+Y zL27AtE%hvRAi9ryXj0Md6CZDSCJIxqtz|=CD3ru*7LG#NNRxHCm~!5{&QRI!)BY_X z`-6@Mgc}k)WEhI=jp&%fw}ITNn3=i9}J#@SS{qubzCd{eX>&76#u7(o?!1KuK@OhfWJ6kW zTh#Ht^%ISM{k|nt$`)Db%>Clv$HVFyRO75?oG9wT|6R(jwUXG^FV*@j96?Cn>=0Z> z=WIi)^3M6K0dykhZRa}T?00-2Ih)gYETeF3rJYRc8`o0Wln(%X&Fl6f<60B%7yR8& zmaaH^;pim4JH9NJzj~sXlgF`sL`Q43#=&NU))oPCZVyd1)aOav?4ikMR9Bg`x7nhK z4T)6H3FZfdLSd6Cqe0A;ViZ&f!l*t445}vQz&_`&I$Q2?$ZG}*k0L&|(HZL0Qr44~ zp9q{=Wn1$CUgovhDCZxwJ@JCadP=AAs#I`HfD90`|NX5#QgYHfK3(Uq4$k?fG98}H zoK2Ht@Rr}yN)_KexB3O0z#MPI$M%71WV}OSUN>@^Ii)c4{C-yoeF(!_1oMY3tDZat z9=zHsLNMWyxs#+N3*^`late;-YP8pxk2@PC3~5E&a)iqK>6pw zl8@|>#?IzB+vSL>wc<@I;UxKgd(g1$=N)6KcVYOx+dPbXnkE}uYf_0_1-7$vUbNxY z&25%kq2KIuo>WBMfbK^GF%{v5m=#IFDC9T7nz{4JZVW=$@3rd4%T8TnD6eaKh=R_F zH=#mKVEy-$MbNN}!_EP(Cj!@o)M=8s*4gP!wJT?Qv4`%mg7)Qq3^6UW-sSZ(@4}pd U!M}vVA5xD~Nwmb46KDVQKXh$)<^TWy literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_disconnected.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..68308692810118b922f48cfdc28f099f09e2c12e GIT binary patch literal 1503 zcmb`He@s(X6vxYtQiPR8E24!8Q$!g4V0~EhA*=-k!l07Mt5^_N)YmE^Y5@_{(N61# zf(VXL6r>geE|Z}Y1XmO>L(o!+rh%l#7F*Nw4IC5Qy%0ycd3_@XuG%=GI+r?-U|^V1?iP{UNrcZap)=c^1XNB zR)Kwo=ygERV_R>`J@(7W!E?hwvJHyFVi_`cn(lN@eeuX<{ZZeWa*!dUX2iQ8&Qslo zzruP{GWC0FnjRh%h2om_<7N#sa%e$3{!*2IA*nSWzrEvd9>OrL^pRKFPwrsrkN6bG zE(+I4{A+;5xehF8J3h=HZ&%2p28=A(;~bmFy@{k^FgEnaNvr+Lno=kuzq9s82(}7D zL?cqL#|Gt}2sndGmwF^YIaVS=ro`sDY4_}<)zg<4jTokfn?iSveBxW~Uf{cM6V1+@ zOe~aIh6jr)<*WzMZo57I^RndbsHz5x8CDR_G)|Q+GF_99YJr1`So1m-+FYC5YlUGX z^j5dJGZe$ZQorskiowG8t-}oZc7-C!cG3+IJcE%kEs)$lA{TkU`S%irXoS2A-c}Q* z^%!r7#?8NGqi+<`WUMLpc_9qq=gl!Xb=i39kY(iiHmP|p`~3X)8i1SbLyPcJz`phJ zJLZa(Agrfda&t#5$cKVECp6pUc2I{{P|PwWuypHumcDIu`&ED| zcW3YW5un=HV4OLrFS?q5g;jcww>H8MgyYMeZuuvN1#B*WS38in8u{j6nZqhf*T+wv z^vrBfUl8Y}2-}aBUj}%&XQqnMTqPI8z56g1&pyi1@%`)H7v*hmaY(FwIQrb+#i`!% zg^Pp#vA2|C*XLe2ah6#D54vTlG@a0Msa~0?warzE>rJobF&ZtXuis)A@)7udCP5|X zi9D)XEj3>P=%I$-x*s4A7QSg5(oL{uBs4>)V~A`#Ox|ci45p9pS4Kd@<_Qg4h;IC+ zT7DEOxWA-@TcPV#+xVt6sV}s4P?YG&J1-Iqyulay2g*3r8lvTu2%M>;Wlwq{f(qk# z2!KEBMiXVt!XY-Uka5H9Vp@+3j0}IQvX*-GXa2K{w^uTVMB+C;KXC zwEf3+i)P^C!n#P=T4qD+@Rj8Y6W9L>dZ!- z6)klQGTQ?y2fvBc%h)w?nzveuM9Cg&_Ykw1r95wKW4e-`ZHjCIC+53QXiBg F`2*AAqIm!S literal 0 HcmV?d00001 diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_normal.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_normal.xml new file mode 100644 index 0000000000..3b1bf33219 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_normal.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_unknown.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_unknown.xml new file mode 100644 index 0000000000..1b6e3ee320 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_fault_code_unknown.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_group_indicator_expanded.9.png b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_ic_group_indicator_expanded.9.png new file mode 100644 index 0000000000000000000000000000000000000000..a00dd55e029d55aef3241e0af1aa8cc8a17272a7 GIT binary patch literal 412 zcmV;N0b~A&P)_!c}4 zUJt~q*5W4gj-mB6co1uVYsPHeki}!z(unS24RCGap2bE4Ev@(|YR5fCTQPKh z7{S<4nqu|iSn2vgqI7*FQ@XyCDqUa8Q@Z^z`BC`E{x2I#N9DRSHt7oh0000U#6MJLtTD-?+SVK(X4A~8-%M~MHk<$oEavQ{o+*(}8Em~I8 zsKk-nVpT=0Qq0LMQdiWB0>fh`XT7j%Qqym*6&W>{MbqBZaRc_?S%a|K$Z7ALY3zQS>_i0000 + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_icon_normal.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_icon_normal.xml new file mode 100644 index 0000000000..79e94d5fde --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_icon_normal.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_progress_bar.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_progress_bar.xml new file mode 100644 index 0000000000..e7ff3ebbbe --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_progress_bar.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_fault_code.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_fault_code.xml new file mode 100644 index 0000000000..44dc12559a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_fault_code.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_group_indicator.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_group_indicator.xml new file mode 100644 index 0000000000..8e59dbfb35 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_group_indicator.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_item_bg.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_item_bg.xml new file mode 100644 index 0000000000..c9bfd36c09 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_item_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_status_icon.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_status_icon.xml new file mode 100644 index 0000000000..c39d48ab99 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_status_icon.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_text_color.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_text_color.xml new file mode 100644 index 0000000000..0b4868e915 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rviz_fmd_selector_text_color.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rvzi_fmd_bg_color_hint_float.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rvzi_fmd_bg_color_hint_float.xml new file mode 100644 index 0000000000..4abb80c031 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/drawable/rvzi_fmd_bg_color_hint_float.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_common_dialog_default_config.xml b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_common_dialog_default_config.xml new file mode 100644 index 0000000000..03d1acb7a3 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools-rviz/src/main/res/layout/rviz_common_dialog_default_config.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +