├── cert.txt ├── META-INF └── MANIFEST.MF ├── SECURITY.md ├── src └── main │ ├── java │ ├── org │ │ └── linktechtips │ │ │ ├── constants │ │ │ ├── SupportConstants.java │ │ │ ├── ServerConstants.java │ │ │ ├── ManageVarType.java │ │ │ ├── MetarSource.java │ │ │ ├── CertificateConstants.java │ │ │ ├── FsdPath.java │ │ │ ├── WeatherConstants.java │ │ │ ├── NetworkConstants.java │ │ │ ├── ClientConstants.java │ │ │ ├── GlobalConstants.java │ │ │ ├── SystemConstants.java │ │ │ └── ProtocolConstants.java │ │ │ ├── process │ │ │ ├── Process.java │ │ │ ├── network │ │ │ │ ├── Allow.java │ │ │ │ ├── Guard.java │ │ │ │ ├── SystemInterface.java │ │ │ │ ├── ClientInterface.java │ │ │ │ ├── TcpInterface.java │ │ │ │ └── ServerInterface.java │ │ │ ├── metar │ │ │ │ ├── Station.java │ │ │ │ ├── Mmq.java │ │ │ │ └── MetarManage.java │ │ │ ├── PMan.java │ │ │ └── config │ │ │ │ ├── ConfigGroup.java │ │ │ │ ├── ConfigEntry.java │ │ │ │ └── ConfigManager.java │ │ │ ├── plugins │ │ │ ├── IPluginService.java │ │ │ └── PluginLoader.java │ │ │ ├── weather │ │ │ ├── Weather.java │ │ │ ├── TempLayer.java │ │ │ ├── CloudLayer.java │ │ │ └── WindLayer.java │ │ │ ├── manager │ │ │ ├── ManageVar.java │ │ │ ├── ManageVarValue.java │ │ │ └── Manage.java │ │ │ ├── httpapi │ │ │ ├── ReadWhazzupController.java │ │ │ ├── ReadWhazzupJsonController.java │ │ │ └── httpApiManage.java │ │ │ ├── MainLauncher.java │ │ │ ├── model │ │ │ ├── Certificate.java │ │ │ ├── Flightplan.java │ │ │ ├── Server.java │ │ │ └── Client.java │ │ │ ├── support │ │ │ └── Support.java │ │ │ └── user │ │ │ ├── AbstractUser.java │ │ │ └── ClientUser.java │ └── module-info.java │ └── resources │ ├── log4j.properties │ └── logback.xml ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md └── workflows │ ├── maven-publish.yml │ ├── maven.yml │ └── codeql.yml ├── README.md ├── fsd.conf └── pom.xml /cert.txt: -------------------------------------------------------------------------------- 1 | 4185 41854185 12 -------------------------------------------------------------------------------- /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: org.linktechtips.MainLauncher 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.0 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Email:3174327625@qq.com 12 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/SupportConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class SupportConstants { 8 | 9 | public static final int L_MAX = 7; 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/Process.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process; 6 | 7 | public abstract class Process { 8 | 9 | public Process() { 10 | } 11 | 12 | public abstract boolean run(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/ServerConstants.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.constants; 7 | 8 | public class ServerConstants { 9 | 10 | public static final int SERVER_METAR = 1; 11 | 12 | public static final int SERVER_SILENT = 2; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/ManageVarType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public final class ManageVarType { 8 | public static final int ATT_INT = 1; 9 | 10 | public static final int ATT_VARCHAR = 5; 11 | 12 | public static final int ATT_DATE = 6; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/MetarSource.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.constants; 7 | 8 | public class MetarSource { 9 | 10 | public final static int SOURCE_NETWORK = 2; 11 | 12 | public final static int SOURCE_FILE = 1; 13 | 14 | public final static int SOURCE_DOWNLOAD = 2; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/network/Allow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.network; 6 | 7 | public class Allow { 8 | private String ip; 9 | 10 | public String getIp() { 11 | return ip; 12 | } 13 | 14 | public void setIp(String ip) { 15 | this.ip = ip; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/plugins/IPluginService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.plugins; 6 | 7 | public interface IPluginService { 8 | // 插件功能主入口方法 9 | void PluginService(); 10 | // 插件名称 11 | // @return 插件名称 12 | String PluginName(); 13 | // 插件版本 14 | // @return 插件版本 15 | String PlugunVersion(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module BetterFSD.Java { 2 | uses org.linktechtips.plugins.IPluginService; 3 | requires org.apache.commons.lang3; 4 | requires java.management; 5 | requires org.apache.commons.io; 6 | requires jdk.httpserver; 7 | requires org.jetbrains.annotations; 8 | requires slf4j.api; 9 | requires java.sql; 10 | requires mysql.connector.java; 11 | requires jbcrypt; 12 | } -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/CertificateConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class CertificateConstants { 8 | public static final String[] CERT_LEVEL = 9 | { 10 | "SUSPENDED", "OBSPILOT", "STUDENT1", "STUDENT2", "STUDENT3", 11 | "CONTROLLER1", "CONTROLLER2", "CONTROLLER3", "INSTRUCTOR1", 12 | "INSTRUCTOR2", "INSTRUCTOR3", "SUPERVISOR", "ADMINISTRATOR" 13 | }; 14 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/FsdPath.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.constants; 7 | 8 | public class FsdPath { 9 | 10 | public static final String PATH_FSD_CONF = "fsd.conf"; 11 | 12 | public static final String PATH_FSD_HELP = "help.txt"; 13 | 14 | public static final String PATH_FSD_MOTD = "motd.txt"; 15 | 16 | public static final String LOGFILE = "log.txt"; 17 | 18 | public static final String METARFILE = "metar.txt"; 19 | 20 | public static final String METARFILENEW = "metarnew.txt"; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/metar/Station.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.metar; 6 | 7 | public class Station { 8 | 9 | private String name; 10 | 11 | private long location; 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public void setName(String name) { 18 | this.name = name; 19 | } 20 | 21 | public long getLocation() { 22 | return location; 23 | } 24 | 25 | public void setLocation(long location) { 26 | this.location = location; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/weather/Weather.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.weather; 6 | 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class Weather { 13 | public static final List wProfiles = new ArrayList<>(); 14 | 15 | public static @Nullable WProfile getWProfile(String name) { 16 | for (WProfile wProfile : wProfiles) { 17 | if (wProfile.getName().equals(name)) { 18 | return wProfile; 19 | } 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 LinkTechTips 3 | # 4 | 5 | log4j.rootLogger = info,stdout 6 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] %m%n 9 | log4j.appender.D=org.apache.log4j.RollingFileAppender 10 | log4j.appender.D.File=logs/BetterFSD.log 11 | log4j.appender.D.Append=true 12 | log4j.appender.D.Threshold=DEBUG 13 | log4j.appender.D.layout = org.apache.log4j.PatternLayout 14 | log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n 15 | log4j.appender.D.MaxFileSize=5MB -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/WeatherConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class WeatherConstants { 8 | public final static int VAR_UPDIRECTION = 0; 9 | public final static int VAR_MIDCOR = 1; 10 | public final static int VAR_LOWCOR = 2; 11 | public final static int VAR_MIDDIRECTION = 3; 12 | public final static int VAR_MIDSPEED = 4; 13 | public final static int VAR_LOWDIRECTION = 5; 14 | public final static int VAR_LOWSPEED = 6; 15 | public final static int VAR_UPTEMP = 7; 16 | public final static int VAR_MIDTEMP = 8; 17 | public final static int VAR_LOWTEMP = 9; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/weather/TempLayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.weather; 6 | 7 | public class TempLayer { 8 | private int ceiling; 9 | private int temp; 10 | 11 | public TempLayer() { 12 | } 13 | 14 | public TempLayer(int ceiling) { 15 | this.ceiling = ceiling; 16 | } 17 | 18 | public int getCeiling() { 19 | return ceiling; 20 | } 21 | 22 | public void setCeiling(int ceiling) { 23 | this.ceiling = ceiling; 24 | } 25 | 26 | public int getTemp() { 27 | return temp; 28 | } 29 | 30 | public void setTemp(int temp) { 31 | this.temp = temp; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/network/Guard.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.network; 6 | 7 | public class Guard { 8 | private long prevTry; 9 | 10 | private String host; 11 | 12 | private int port; 13 | 14 | public long getPrevTry() { 15 | return prevTry; 16 | } 17 | 18 | public void setPrevTry(long prevTry) { 19 | this.prevTry = prevTry; 20 | } 21 | 22 | public String getHost() { 23 | return host; 24 | } 25 | 26 | public void setHost(String host) { 27 | this.host = host; 28 | } 29 | 30 | public int getPort() { 31 | return port; 32 | } 33 | 34 | public void setPort(int port) { 35 | this.port = port; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/manager/ManageVar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.manager; 6 | 7 | public class ManageVar { 8 | private int type; 9 | 10 | private String name; 11 | 12 | private ManageVarValue value; 13 | 14 | public int getType() { 15 | return type; 16 | } 17 | 18 | public void setType(int type) { 19 | this.type = type; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public ManageVarValue getValue() { 31 | return value; 32 | } 33 | 34 | public void setValue(ManageVarValue value) { 35 | this.value = value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/manager/ManageVarValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.manager; 6 | 7 | public class ManageVarValue { 8 | private int number; 9 | 10 | private String string; 11 | 12 | private long timeVal; 13 | 14 | public int getNumber() { 15 | return number; 16 | } 17 | 18 | public void setNumber(int number) { 19 | this.number = number; 20 | } 21 | 22 | public String getString() { 23 | return string; 24 | } 25 | 26 | public void setString(String string) { 27 | this.string = string; 28 | } 29 | 30 | public long getTimeVal() { 31 | return timeVal; 32 | } 33 | 34 | public void setTimeVal(long timeVal) { 35 | this.timeVal = timeVal; 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/PMan.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class PMan { 14 | private final static Logger LOGGER = LoggerFactory.getLogger(PMan.class); 15 | 16 | public static final List processes = new ArrayList<>(); 17 | 18 | private boolean busy; 19 | 20 | public PMan() { 21 | busy = false; 22 | } 23 | 24 | public void registerProcess(Process process) { 25 | processes.add(process); 26 | } 27 | 28 | public void run() { 29 | for (Process process : processes) { 30 | if (process.run()) { 31 | busy = true; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request \U0001F451" 3 | about: 对程序的需求或建议 / Suggest an idea for program 4 | title: "\U0001F451[Enhancement] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | ### 🥰 需求描述(Description) 18 | 19 | 23 | 24 | ### 🧐 解决方案(Solution) 25 | 26 | 30 | 31 | ### 🚑 其他信息(Other Information) 32 | 33 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Question \U0001F9D0" 3 | about: 对程序使用的疑问或需要帮助 / Questions about the use of the program or need help 4 | title: "\U0001F9D0[Question] " 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | ### 🧐 问题描述(Description) 18 | 19 | 23 | 24 | ### 🚑 其他信息(Other Information) 25 | 26 | 30 | 31 | - 程序版本(Program Version): 32 | - 系统版本号(OS Version): 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BetterFSD Java Edition 2 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FLinkTechTips%2FBetterFSD-Java.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FLinkTechTips%2FBetterFSD-Java?ref=badge_shield) 3 | 4 | ## 关于 5 | 本项目为 "Marty Bochane's FSD 2" C版本的Java重写,有问题欢迎提交issue或PR,您还可以加入用户交流QQ群 [723619450](https://jq.qq.com/?_wv=1027&k=Gugroyas) 6 | ## 君子协议 7 | 本项目基于 GPL-3.0 协议开源 若要使用本项目 请给本项目一个star 8 | 9 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FLinkTechTips%2FBetterFSD-Java.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FLinkTechTips%2FBetterFSD-Java?ref=badge_large) 10 | 11 | GPL-3.0第7节中的附加条款 12 | 13 | 分发软件的修改版本时,必须以合理的方式更改软件名称或版本号,以便将其与原始版本区分开来 14 | 15 | 不得删除软件中显示的版权声明 16 | ## 已实现功能 17 | * 插件管理器 18 | * whazzup的txt输出和json输出 19 | * whazzup的HTTPAPI 20 | * 秒级whazzup更新 21 | * whazzup航向输出 22 | * 更稳定的FSD 23 | * ECHO防崩溃 24 | * 用户上下线记录 25 | * 使用mySQL作为cert数据库 26 | ## 待实现功能 27 | * 内置PDC 28 | * 内置语音实现 29 | * Remark同步 30 | * !!BetterFSD 命令,用于服务器管理 31 | ## 已知问题 32 | * 无法操控Euroscope Sweatbox中的机组 -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 19 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '19' 25 | distribution: 'temurin' 26 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 27 | settings-path: ${{ github.workspace }} # location for the settings.xml file 28 | 29 | - name: Build with Maven 30 | run: mvn -B package --file pom.xml 31 | 32 | 33 | - name: Publish to GitHub Packages Apache Maven 34 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.PACKAGE }} 37 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/NetworkConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class NetworkConstants { 8 | public static final int KILL_NONE = 0; 9 | 10 | public static final int KILL_COMMAND = 1; 11 | 12 | public static final int KILL_FLOOD = 2; 13 | 14 | public static final int KILL_INIT_TIMEOUT = 3; 15 | 16 | public static final int KILL_DATA_TIMEOUT = 4; 17 | 18 | public static final int KILL_CLOSED = 5; 19 | 20 | public static final int KILL_WRITE_ERR = 6; 21 | 22 | public static final int KILL_KILL = 7; 23 | 24 | public static final int KILL_PROTOCOL = 8; 25 | 26 | public static final int FEED_IN = 1; 27 | 28 | public static final int FEED_OUT = 2; 29 | 30 | public static final int FEED_BOTH = 3; 31 | 32 | public static final String[] KILL_REASONS = { 33 | "", 34 | "closed on command", 35 | "flooding", 36 | "initial timeout", 37 | "socket stalled", 38 | "connection closed", 39 | "write error", 40 | "killed on command", 41 | "protocol revision error" 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up JDK 19 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '19' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | - name: Update dependency graph 35 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 36 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/weather/CloudLayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.weather; 6 | 7 | public class CloudLayer { 8 | private int ceiling; 9 | private int floor; 10 | private int coverage; 11 | private int icing; 12 | private int turbulence; 13 | 14 | public CloudLayer() { 15 | } 16 | 17 | public CloudLayer(int ceiling, int floor) { 18 | this.ceiling = ceiling; 19 | this.floor = floor; 20 | } 21 | 22 | public int getCeiling() { 23 | return ceiling; 24 | } 25 | 26 | public void setCeiling(int ceiling) { 27 | this.ceiling = ceiling; 28 | } 29 | 30 | public int getFloor() { 31 | return floor; 32 | } 33 | 34 | public void setFloor(int floor) { 35 | this.floor = floor; 36 | } 37 | 38 | public int getCoverage() { 39 | return coverage; 40 | } 41 | 42 | public void setCoverage(int coverage) { 43 | this.coverage = coverage; 44 | } 45 | 46 | public int getIcing() { 47 | return icing; 48 | } 49 | 50 | public void setIcing(int icing) { 51 | this.icing = icing; 52 | } 53 | 54 | public int getTurbulence() { 55 | return turbulence; 56 | } 57 | 58 | public void setTurbulence(int turbulence) { 59 | this.turbulence = turbulence; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/metar/Mmq.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.process.metar; 7 | 8 | public class Mmq { 9 | 10 | private String destination; 11 | 12 | private String metarId; 13 | 14 | private int fd; 15 | 16 | private int parsed; 17 | 18 | private Mmq prev; 19 | 20 | private Mmq next; 21 | 22 | public String getDestination() { 23 | return destination; 24 | } 25 | 26 | public void setDestination(String destination) { 27 | this.destination = destination; 28 | } 29 | 30 | public String getMetarId() { 31 | return metarId; 32 | } 33 | 34 | public void setMetarId(String metarId) { 35 | this.metarId = metarId; 36 | } 37 | 38 | public int getFd() { 39 | return fd; 40 | } 41 | 42 | public void setFd(int fd) { 43 | this.fd = fd; 44 | } 45 | 46 | public int isParsed() { 47 | return parsed; 48 | } 49 | 50 | public void setParsed(int parsed) { 51 | this.parsed = parsed; 52 | } 53 | 54 | public Mmq getPrev() { 55 | return prev; 56 | } 57 | 58 | public void setPrev(Mmq prev) { 59 | this.prev = prev; 60 | } 61 | 62 | public Mmq getNext() { 63 | return next; 64 | } 65 | 66 | public void setNext(Mmq next) { 67 | this.next = next; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report \U0001F41B" 3 | about: 创建 Bug 报告以帮助我们改进 / Create a report to help us improve 4 | title: "\U0001F41B[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 18 | 19 | ### 🐛 描述(Description) 20 | 24 | 25 | 26 | ### 📷 复现步骤(Steps to Reproduce) 27 | 28 | 34 | 1. 35 | 2. 36 | 3. 37 | 38 | ### 📄 日志信息(Log Information) 39 | 47 | 48 | ### 🚑 基本信息(Basic Information) 49 | 50 | - 程序版本(Program Version): 51 | - 系统版本号(OS Version): 52 | 53 | ### 🖼 截图(Screenshots) 54 | 55 | 59 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/plugins/PluginLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.plugins; 6 | 7 | import java.io.File; 8 | import java.net.MalformedURLException; 9 | import java.net.URL; 10 | import java.net.URLClassLoader; 11 | import java.util.*; 12 | 13 | public class PluginLoader { 14 | public static final String PLUGIN_PATH = "plugins"; 15 | 16 | public static List loadPlugins() throws MalformedURLException { 17 | List plugins = new ArrayList<>(); 18 | 19 | File parentDir = new File(PLUGIN_PATH); 20 | File[] files = parentDir.listFiles(); 21 | if (null == files) { 22 | return Collections.emptyList(); 23 | } 24 | 25 | // 从目录下筛选出所有jar文件 26 | List jarFiles = Arrays.stream(files) 27 | .filter(file -> file.getName().endsWith(".jar")).toList(); 28 | 29 | URL[] urls = new URL[jarFiles.size()]; 30 | for (int i = 0; i < jarFiles.size(); i++) { 31 | // 加上 "file:" 前缀表示本地文件 32 | urls[i] = new URL("file:" + jarFiles.get(i).getAbsolutePath()); 33 | } 34 | 35 | URLClassLoader urlClassLoader = new URLClassLoader(urls); 36 | // 使用 ServiceLoader 以SPI的方式加载插件包中的 PluginService 实现类 37 | ServiceLoader serviceLoader = ServiceLoader.load(IPluginService.class, urlClassLoader); 38 | for (IPluginService IPluginService : serviceLoader) { 39 | plugins.add(IPluginService); 40 | } 41 | return plugins; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/weather/WindLayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.weather; 6 | 7 | public class WindLayer { 8 | private int ceiling; 9 | private int floor; 10 | private int direction; 11 | private int speed; 12 | private int gusting; 13 | private int turbulence; 14 | 15 | public WindLayer() { 16 | } 17 | 18 | public WindLayer(int ceiling, int floor) { 19 | this.ceiling = ceiling; 20 | this.floor = floor; 21 | } 22 | 23 | public int getCeiling() { 24 | return ceiling; 25 | } 26 | 27 | public void setCeiling(int ceiling) { 28 | this.ceiling = ceiling; 29 | } 30 | 31 | public int getFloor() { 32 | return floor; 33 | } 34 | 35 | public void setFloor(int floor) { 36 | this.floor = floor; 37 | } 38 | 39 | public int getDirection() { 40 | return direction; 41 | } 42 | 43 | public void setDirection(int direction) { 44 | this.direction = direction; 45 | } 46 | 47 | public int getSpeed() { 48 | return speed; 49 | } 50 | 51 | public void setSpeed(int speed) { 52 | this.speed = speed; 53 | } 54 | 55 | public int getGusting() { 56 | return gusting; 57 | } 58 | 59 | public void setGusting(int gusting) { 60 | this.gusting = gusting; 61 | } 62 | 63 | public int getTurbulence() { 64 | return turbulence; 65 | } 66 | 67 | public void setTurbulence(int turbulence) { 68 | this.turbulence = turbulence; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/ClientConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class ClientConstants { 8 | 9 | public static final int CLIENT_PILOT = 1; 10 | 11 | public static final int CLIENT_ATC = 2; 12 | 13 | public static final int CLIENT_ALL = 3; 14 | 15 | public static final String[] CL_CMD_NAMES = { 16 | "#AA", 17 | "#DA", 18 | "#AP", 19 | "#DP", 20 | "$HO", 21 | "#TM", 22 | "#RW", 23 | "@", 24 | "%", 25 | "$PI", 26 | "$PO", 27 | "$HA", 28 | "$FP", 29 | "#SB", 30 | "#PC", 31 | "#WX", 32 | "#CD", 33 | "#WD", 34 | "#TD", 35 | "$C?", 36 | "$CI", 37 | "$AX", 38 | "$AR", 39 | "$ER", 40 | "$CQ", 41 | "$CR", 42 | "$!!", 43 | "#DL" 44 | }; 45 | 46 | public static final String[] ERR_STR = { 47 | "No error", 48 | "Callsign in use", 49 | "Invalid callsign", 50 | "Already registerd", 51 | "Syntax error", 52 | "Invalid source callsign", 53 | "Invalid CID/password", 54 | "No such callsign", 55 | "No flightplan", 56 | "No such weather profile", 57 | "Invalid protocol revision", 58 | "Requested level too high", 59 | "Too many clients connected", 60 | "CID/PID was suspended", 61 | "Cannot log in as Observer" 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/config/ConfigGroup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.config; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Objects; 13 | 14 | public class ConfigGroup { 15 | 16 | private String name; 17 | 18 | private final @NotNull List entries; 19 | 20 | private boolean changed; 21 | 22 | public ConfigGroup(String name) { 23 | this.name = name; 24 | entries = new ArrayList<>(); 25 | int nEntries = 0; 26 | changed = true; 27 | } 28 | 29 | public @Nullable ConfigEntry getEntry(String name) { 30 | for (ConfigEntry entry : entries) { 31 | if (Objects.equals(entry.getVar(), name)) { 32 | return entry; 33 | } 34 | } 35 | 36 | return null; 37 | } 38 | 39 | public void createEntry(String var, String data) { 40 | ConfigEntry configEntry = new ConfigEntry(var, data); 41 | entries.add(configEntry); 42 | } 43 | 44 | public void handleEntry(String var, String data) { 45 | ConfigEntry entry = getEntry(var); 46 | if (entry == null) { 47 | createEntry(var, data); 48 | changed = true; 49 | return; 50 | } 51 | 52 | if (Objects.equals(entry.getData(), data)) { 53 | return; 54 | } 55 | 56 | entry.setData(data); 57 | changed = true; 58 | } 59 | 60 | public String getName() { 61 | return name; 62 | } 63 | 64 | public void setName(String name) { 65 | this.name = name; 66 | } 67 | 68 | public boolean isChanged() { 69 | return changed; 70 | } 71 | 72 | public void setChanged(boolean changed) { 73 | this.changed = changed; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/httpapi/ReadWhazzupController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.httpapi; 6 | 7 | import com.sun.net.httpserver.HttpExchange; 8 | import com.sun.net.httpserver.HttpHandler; 9 | import org.apache.commons.io.FileUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.io.File; 14 | import java.io.OutputStream; 15 | import java.nio.charset.StandardCharsets; 16 | 17 | import static org.linktechtips.httpapi.httpApiManage.whazzupFile; 18 | public class ReadWhazzupController implements HttpHandler { 19 | private final static Logger LOGGER = LoggerFactory.getLogger(ReadWhazzupController.class); 20 | @Override 21 | public void handle(HttpExchange httpExchange) { 22 | try { 23 | StringBuilder responseText = new StringBuilder(); 24 | String whazzup = FileUtils.readFileToString(new File(whazzupFile),StandardCharsets.UTF_8); 25 | responseText.append(whazzup); 26 | handleResponse(httpExchange, responseText.toString()); 27 | } catch (NullPointerException e) { 28 | LOGGER.info("[HTTP/ReadWhazzup]: Cannot find whazzup file"); 29 | e.printStackTrace(); 30 | } catch (Exception ex) { 31 | ex.printStackTrace(); 32 | } 33 | } 34 | private void handleResponse(HttpExchange httpExchange, String responsetext) throws Exception { 35 | //生成html 36 | byte[] responseContentByte = responsetext.getBytes(StandardCharsets.UTF_8); 37 | 38 | //设置响应头,必须在sendResponseHeaders方法之前设置! 39 | httpExchange.getResponseHeaders().add("Content-Type:", "text/plain;charset=utf-8"); 40 | 41 | //设置响应码和响应体长度,必须在getResponseBody方法之前调用! 42 | httpExchange.sendResponseHeaders(200, responseContentByte.length); 43 | 44 | OutputStream out = httpExchange.getResponseBody(); 45 | out.write(responseContentByte); 46 | out.flush(); 47 | out.close(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/httpapi/ReadWhazzupJsonController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.httpapi; 6 | 7 | import com.sun.net.httpserver.HttpExchange; 8 | import com.sun.net.httpserver.HttpHandler; 9 | import org.apache.commons.io.FileUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.io.File; 14 | import java.io.OutputStream; 15 | import java.nio.charset.StandardCharsets; 16 | 17 | import static org.linktechtips.httpapi.httpApiManage.whazzupJsonFile; 18 | 19 | public class ReadWhazzupJsonController implements HttpHandler { 20 | private final static Logger LOGGER = LoggerFactory.getLogger(ReadWhazzupJsonController.class); 21 | @Override 22 | public void handle(HttpExchange httpExchange) { 23 | try { 24 | StringBuilder responseText = new StringBuilder(); 25 | String whazzup = FileUtils.readFileToString(new File(whazzupJsonFile),StandardCharsets.UTF_8); 26 | responseText.append(whazzup); 27 | handleResponse(httpExchange, responseText.toString()); 28 | } catch (NullPointerException e) { 29 | LOGGER.info("[HTTP/ReadWhazzupJson]: Cannot find whazzup file"); 30 | e.printStackTrace(); 31 | } catch (Exception ex) { 32 | ex.printStackTrace(); 33 | } 34 | } 35 | private void handleResponse(HttpExchange httpExchange, String responsetext) throws Exception { 36 | //生成html 37 | byte[] responseContentByte = responsetext.getBytes(StandardCharsets.UTF_8); 38 | 39 | //设置响应头,必须在sendResponseHeaders方法之前设置! 40 | httpExchange.getResponseHeaders().add("Content-Type:", "application/json;charset=utf-8"); 41 | 42 | //设置响应码和响应体长度,必须在getResponseBody方法之前调用! 43 | httpExchange.sendResponseHeaders(200, responseContentByte.length); 44 | 45 | OutputStream out = httpExchange.getResponseBody(); 46 | out.write(responseContentByte); 47 | out.flush(); 48 | out.close(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | BetterFSD 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ${log.pattern} 22 | 23 | 24 | 25 | 26 | 27 | ${log.pattern} 28 | 29 | 30 | 31 | 32 | ${log.file} 33 | 5MB 34 | 35 | 36 | true 37 | 38 | 39 | 40 | 5000 41 | 0 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/config/ConfigEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.config; 6 | 7 | import org.apache.commons.lang3.math.NumberUtils; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Objects; 14 | 15 | public class ConfigEntry { 16 | 17 | private String var; 18 | 19 | private String data; 20 | 21 | private final @NotNull List parts; 22 | 23 | private int nParts; 24 | 25 | public ConfigEntry(String var, String data) { 26 | this.var = var; 27 | this.data = data; 28 | boolean changed = true; 29 | parts = new ArrayList<>(); 30 | } 31 | 32 | public String getData() { 33 | return data; 34 | } 35 | 36 | public void setData(String data) { 37 | this.data = data; 38 | } 39 | 40 | public int getInt() { 41 | return NumberUtils.toInt(data); 42 | } 43 | 44 | public void fillParts() { 45 | String[] split = data.split(","); 46 | for (String part : split) { 47 | parts.add(part.strip()); 48 | } 49 | nParts = split.length; 50 | } 51 | 52 | public int inList(String entry) { 53 | if (parts.isEmpty()) { 54 | fillParts(); 55 | } 56 | for (String part : parts) { 57 | if (Objects.equals(part, entry)) { 58 | return 1; 59 | } 60 | } 61 | return 0; 62 | } 63 | 64 | public int getNParts() { 65 | if (parts.isEmpty()) { 66 | fillParts(); 67 | } 68 | return nParts; 69 | } 70 | 71 | public @Nullable String getPart(int num) { 72 | if (parts.isEmpty()) { 73 | fillParts(); 74 | } 75 | if (num >= nParts) { 76 | return null; 77 | } 78 | 79 | return parts.get(num); 80 | } 81 | 82 | public String getVar() { 83 | return var; 84 | } 85 | 86 | public void setVar(String var) { 87 | this.var = var; 88 | } 89 | 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/MainLauncher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips; 6 | 7 | import org.linktechtips.constants.FsdPath; 8 | import org.linktechtips.constants.GlobalConstants; 9 | import org.linktechtips.support.Support; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.lang.management.ManagementFactory; 14 | 15 | import java.io.File; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.util.List; 19 | 20 | public class MainLauncher { 21 | private static String[] args; 22 | 23 | public static void main(String[] args) { 24 | System.setProperty("java.net.preferIPv4Stack", "true"); 25 | System.setProperty("server.name", "BetterFSD"); 26 | final Logger LOGGER = LoggerFactory.getLogger(MainLauncher.class); 27 | MainLauncher.args = args; 28 | String property = System.getProperty("user.dir"); 29 | Path path = Paths.get(FsdPath.PATH_FSD_CONF); 30 | File file = new File(FsdPath.PATH_FSD_CONF); 31 | LOGGER.info(String.format("BetterFSD Java Edition Version %s", GlobalConstants.VERSION)); 32 | LOGGER.info(String.format("Operating System: %s", System.getProperty("os.name"))); 33 | LOGGER.info(String.format("Java Version: %s, %s", System.getProperty("java.version"), 34 | System.getProperty("java.vendor"))); 35 | LOGGER.info(String.format("Java VM Version: %s, %s", System.getProperty("java.vm.specification.version"), 36 | System.getProperty("java.vm.specification.vendor"))); 37 | List JvmFlag = ManagementFactory.getRuntimeMXBean().getInputArguments(); 38 | LOGGER.info(String.format("JVM Flags: %s", JvmFlag)); 39 | LOGGER.info(String.format("Cores: %s", Runtime.getRuntime().availableProcessors())); 40 | LOGGER.info(String.format("[BetterFSD]: Using config file: %s", file.getAbsolutePath())); 41 | String configFile = FsdPath.PATH_FSD_CONF; 42 | doSignals(); 43 | run(configFile); 44 | } 45 | private static void run(String configFile) { 46 | Main main = new Main(configFile); 47 | while (true) { 48 | main.run(); 49 | } 50 | } 51 | private static void doSignals() { 52 | Support.startTimer(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/GlobalConstants.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.constants; 7 | 8 | public class GlobalConstants { 9 | public static final String PRODUCT = "BetterFSD Java Edition Version 1.1.2-hotfix1 By 4185 QQ:3174327625"; 10 | public static final String VERSION = "V1.1.2-hotfix1"; 11 | public static final int NEED_REVISION = 9; 12 | 13 | /** 14 | * WARNING!!!: The USER_TIMEOUT (idle time of a SOCKET before it's dropped) 15 | * should not be higher than the SERVER_TIMEOUT (idle time of a 16 | * server) 17 | */ 18 | public static final int USER_TIMEOUT = 500_000; 19 | public static final int SERVER_TIMEOUT = 800_000; 20 | public static final int CLIENT_TIMEOUT = 800_000; 21 | public static final int SILENT_CLIENT_TIMEOUT = 36000_000; 22 | public static final int WIND_DELTA_TIMEOUT = 70_000; 23 | 24 | public static final int USER_PING_TIMEOUT = 200_000; 25 | public static final int USER_FEED_CHECK = 3000; 26 | public static final int LAG_CHECK = 60_000; 27 | public static final int NOTIFY_CHECK = 300_000; 28 | public static final int SYNC_TIMEOUT = 120_000; 29 | public static final int SERVER_MAX_TOOK = 240; 30 | public static final int MAX_HOPS = 10; 31 | public static final int GUARD_RETRY = 120_000; 32 | public static final int CALLSIGN_BYTES = 12; 33 | public static final int MAX_LINE_LENGTH = 1026; 34 | public static final int MAX_METAR_DOWNLOAD_TIME = 1600_000; 35 | public static final int CERT_FILE_CHECK = 30_000; 36 | 37 | public static final int WHAZZUP_CHECK = 1_000; 38 | public static final int CONNECT_DELAY = 20_000; 39 | 40 | public static final int LEV_SUSPENDED = 0; 41 | public static final int LEV_OBSPILOT = 1; 42 | public static final int LEV_STUDENT1 = 2; 43 | public static final int LEV_STUDENT2 = 3; 44 | public static final int LEV_STUDENT3 = 4; 45 | public static final int LEV_CONTROLLER1 = 5; 46 | public static final int LEV_CONTROLLER2 = 6; 47 | public static final int LEV_CONTROLLER3 = 7; 48 | public static final int LEV_INSTRUCTOR1 = 8; 49 | public static final int LEV_INSTRUCTOR2 = 9; 50 | public static final int LEV_INSTRUCTOR3 = 10; 51 | public static final int LEV_SUPERVISOR = 11; 52 | public static final int LEV_ADMINISTRATOR = 12; 53 | public static final int LEV_MAX = 12; 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/httpapi/httpApiManage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.httpapi; 6 | 7 | import org.linktechtips.process.config.ConfigEntry; 8 | import org.linktechtips.process.config.ConfigGroup; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import com.sun.net.httpserver.HttpServer; 12 | 13 | import java.io.IOException; 14 | import java.net.InetSocketAddress; 15 | import java.util.concurrent.Executors; 16 | 17 | import static org.linktechtips.Main.configManager; 18 | 19 | public class httpApiManage { 20 | private final static Logger LOGGER = LoggerFactory.getLogger(httpApiManage.class); 21 | public static String whazzupFile; 22 | public static String whazzupJsonFile; 23 | public static String webServerPort; 24 | public static int webServerPortInt; 25 | public httpApiManage() { 26 | readConfig(); 27 | HttpServer httpServer; 28 | if (webServerPort == null){ 29 | LOGGER.error("[HTTP]: WebServerPort is null"); 30 | LOGGER.error("[HTTP]: exit"); 31 | } else { 32 | LOGGER.info(String.format("[HTTP]: HTTP Server Will Run On Port %s", webServerPort)); 33 | try { 34 | httpServer = HttpServer.create(new InetSocketAddress(webServerPortInt), 0);httpServer.createContext("/api/whazzup/txt", new ReadWhazzupController()); 35 | httpServer.createContext("/api/whazzup/json", new ReadWhazzupJsonController()); 36 | httpServer.setExecutor(Executors.newFixedThreadPool(10)); 37 | httpServer.start(); 38 | } catch (IOException e) { 39 | LOGGER.info("[HTTP]: I/O Exception"); 40 | e.printStackTrace(); 41 | } 42 | } 43 | } 44 | public void readConfig() { 45 | ConfigEntry entry; 46 | ConfigGroup system = configManager.getGroup("system"); 47 | if (system != null) { 48 | if ((entry = system.getEntry("whazzup")) != null) { 49 | whazzupFile = entry.getData(); 50 | } 51 | if ((entry = system.getEntry("whazzupjson")) != null) { 52 | whazzupJsonFile= entry.getData(); 53 | } 54 | if ((entry = system.getEntry("webserverport")) != null) { 55 | webServerPort = entry.getData(); 56 | } 57 | if ((entry = system.getEntry("webserverport")) != null) { 58 | webServerPortInt = entry.getInt(); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '38 4 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - uses: actions/checkout@v3 59 | - name: Set up JDK 19 60 | uses: actions/setup-java@v3 61 | with: 62 | java-version: '19' 63 | distribution: 'temurin' 64 | cache: maven 65 | - name: Build with Maven 66 | run: mvn -B package --file pom.xml 67 | 68 | # ℹ️ Command-line programs to run using the OS shell. 69 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 70 | 71 | # If the Autobuild fails above, remove it and uncomment the following three lines. 72 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 73 | 74 | # - run: | 75 | # echo "Run, Build Application using script" 76 | # ./location_of_script_within_repo/buildscript.sh 77 | 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@v2 80 | with: 81 | category: "/language:${{matrix.language}}" 82 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/SystemConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class SystemConstants { 8 | 9 | public static final int SYS_SERVERS = 0; 10 | public static final int SYS_INFORMATION = 1; 11 | public static final int SYS_PING = 2; 12 | public static final int SYS_CONNECT = 3; 13 | public static final int SYS_TIME = 4; 14 | public static final int SYS_ROUTE = 5; 15 | public static final int SYS_WEATHER = 6; 16 | public static final int SYS_DISCONNECT = 7; 17 | public static final int SYS_HELP = 8; 18 | public static final int SYS_QUIT = 9; 19 | public static final int SYS_STAT = 10; 20 | public static final int SYS_SAY = 11; 21 | public static final int SYS_CLIENTS = 12; 22 | public static final int SYS_CERT = 13; 23 | public static final int SYS_PWD = 14; 24 | public static final int SYS_DISTANCE = 15; 25 | public static final int SYS_RANGE = 16; 26 | public static final int SYS_LOG = 17; 27 | public static final int SYS_WALL = 18; 28 | public static final int SYS_DELGUARD = 19; 29 | public static final int SYS_METAR = 20; 30 | public static final int SYS_WP = 21; 31 | public static final int SYS_KILL = 22; 32 | public static final int SYS_POS = 23; 33 | public static final int SYS_DUMP = 24; 34 | public static final int SYS_SERVERS2 = 25; 35 | public static final int SYS_REFMETAR = 26; 36 | 37 | public static final String[] SYS_CMDS = { 38 | "servers", 39 | "info", 40 | "ping", 41 | "connect", 42 | "time", 43 | "route", 44 | "weather", 45 | "disconnect", 46 | "help", 47 | "quit", 48 | "stat", 49 | "say", 50 | "clients", 51 | "cert", 52 | "pwd", 53 | "distance", 54 | "range", 55 | "log", 56 | "wall", 57 | "delguard", 58 | "metar", 59 | "wp", 60 | "kill", 61 | "pos", 62 | "dump", 63 | "servers2", 64 | "refreshmetar" 65 | }; 66 | 67 | public static final int[] NEED_AUTHORIZATION = { 68 | 0, /* servers */ 69 | 1, /* info */ 70 | 1, /* ping */ 71 | 1, /* connect */ 72 | 0, /* time */ 73 | 0, /* route */ 74 | 0, /* weather */ 75 | 1, /* disconnect */ 76 | 0, /* help */ 77 | 0, /* quit */ 78 | 1, /* stat */ 79 | 1, /* say */ 80 | 1, /* clients */ 81 | 1, /* cert */ 82 | 0, /* pwd */ 83 | 1, /* distance */ 84 | 1, /* range */ 85 | 1, /* log */ 86 | 1, /* wall */ 87 | 1, /* delguard */ 88 | 0, /* metar */ 89 | 1, /* wp */ 90 | 1, /* kill */ 91 | 0, /* pos */ 92 | 1, /* dump */ 93 | 0, /* servers2 */ 94 | 1 /* refresh metar */ 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/manager/Manage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.manager; 6 | 7 | import org.linktechtips.constants.ManageVarType; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Date; 13 | import java.util.List; 14 | import java.util.Objects; 15 | 16 | public class Manage { 17 | 18 | public static Manage manager; 19 | 20 | private int nVars; 21 | 22 | private final @NotNull List variables; 23 | 24 | public Manage() { 25 | nVars = 0; 26 | variables = new ArrayList<>(); 27 | } 28 | 29 | public int addVar(String name, int type) { 30 | ManageVar var = new ManageVar(); 31 | var.setType(type); 32 | var.setName(name); 33 | var.setValue(new ManageVarValue()); 34 | ++nVars; 35 | variables.add(var); 36 | return variables.size() - 1; 37 | } 38 | 39 | public void delVar(int num) { 40 | if (variables.size() > num) { 41 | variables.remove(num); 42 | } 43 | } 44 | 45 | public void incVar(int num) { 46 | ManageVar var = variables.get(num); 47 | ManageVarValue value = var.getValue(); 48 | value.setNumber(value.getNumber() + 1); 49 | } 50 | 51 | public void decVar(int num) { 52 | ManageVar var = variables.get(num); 53 | ManageVarValue value = var.getValue(); 54 | value.setNumber(value.getNumber() - 1); 55 | } 56 | 57 | public void setVar(int num, int number) { 58 | ManageVar var = variables.get(num); 59 | ManageVarValue value = var.getValue(); 60 | value.setNumber(number); 61 | } 62 | 63 | public void setVar(int num, String str) { 64 | ManageVar var = variables.get(num); 65 | ManageVarValue value = var.getValue(); 66 | value.setString(str); 67 | } 68 | 69 | public void setVar(int num, long timeVal) { 70 | ManageVar var = variables.get(num); 71 | ManageVarValue value = var.getValue(); 72 | value.setTimeVal(timeVal); 73 | } 74 | 75 | public @Nullable ManageVar getVar(int num) { 76 | if (num >= nVars) { 77 | return null; 78 | } 79 | return variables.get(num); 80 | } 81 | 82 | public int getNVars() { 83 | return nVars; 84 | } 85 | 86 | public @Nullable String sprintValue(int num) { 87 | if (num >= nVars || variables.get(num).getName() == null) { 88 | return null; 89 | } 90 | ManageVar var = variables.get(num); 91 | 92 | return switch (var.getType()) { 93 | case ManageVarType 94 | .ATT_INT -> String.valueOf(var.getValue().getNumber()); 95 | case ManageVarType.ATT_VARCHAR -> var.getValue().getString(); 96 | case ManageVarType.ATT_DATE -> new Date(var.getValue().getTimeVal()).toString(); 97 | default -> ""; 98 | }; 99 | } 100 | 101 | public int getVarNum(String name) { 102 | for (int i = 0; i < variables.size(); i++) { 103 | ManageVar variable = variables.get(i); 104 | if (Objects.equals(variable.getName(), name)) { 105 | return i; 106 | } 107 | } 108 | 109 | return -1; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/network/SystemInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.network; 6 | 7 | import org.linktechtips.model.Server; 8 | import org.linktechtips.user.AbstractUser; 9 | import org.linktechtips.user.SystemUser; 10 | import org.linktechtips.support.Support; 11 | import org.linktechtips.weather.WProfile; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.nio.channels.SocketChannel; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Scanner; 18 | 19 | public class SystemInterface extends TcpInterface { 20 | public SystemInterface(int port, String code, String d) { 21 | super(port, code, d); 22 | 23 | } 24 | 25 | public boolean run() { 26 | return super.run(); 27 | } 28 | 29 | public void newUser(SocketChannel fd, String peer, int portnum, int g) { 30 | insertUser(new SystemUser(fd, this, peer, portnum, g)); 31 | } 32 | 33 | public void receivePong(String from, @NotNull String data, String pc, String hops) { 34 | int fd; 35 | long now; 36 | Scanner scanner = new Scanner(data); 37 | if (!scanner.hasNextInt()) { 38 | return; 39 | } 40 | fd = scanner.nextInt(); 41 | if (!scanner.hasNextLong()) { 42 | return; 43 | } 44 | now = scanner.nextLong(); 45 | if (fd == -1) { 46 | return; 47 | } 48 | for (AbstractUser temp : users) { 49 | if (temp.getFd() == fd) { 50 | temp.uprintf("\r\nPONG received from %s: %d seconds (%s,%s)\r\n", 51 | from, Support.mtime() - now, pc, hops); 52 | temp.printPrompt(); 53 | return; 54 | } 55 | } 56 | } 57 | 58 | public void receiveWeather(int fd, @NotNull WProfile w) { 59 | if (fd == -2) { 60 | String buffer = w.print(); 61 | List array = new ArrayList<>(); 62 | int count = Support.breakPacket(buffer, array, 100); 63 | WProfile wp = new WProfile(w.getName(), Support.mgmtime(), Server.myServer.getIdent()); 64 | wp.loadArray(array.toArray(new String[0]), count); 65 | return; 66 | } 67 | for (AbstractUser temp : users) { 68 | if (temp.getFd() == fd) { 69 | SystemUser st = (SystemUser) temp; 70 | w.fix(st.getLat(), st.getLon()); 71 | st.printWeather(w); 72 | break; 73 | } 74 | } 75 | } 76 | 77 | public void receiveMetar(int fd, String wp, String w) { 78 | for (AbstractUser temp : users) { 79 | if (temp.getFd() == fd) { 80 | SystemUser st = (SystemUser) temp; 81 | st.printMetar(wp, w); 82 | break; 83 | } 84 | } 85 | } 86 | 87 | public void receiveNoWx(int fd, String st) { 88 | if (fd == -2) { 89 | WProfile wp = new WProfile(st, Support.mgmtime(), Server.myServer.getIdent()); 90 | } 91 | for (AbstractUser temp : users) { 92 | if (temp.getFd() == fd) { 93 | temp.uprintf("\r\nNo weather available for %s.\r\n", st); 94 | temp.printPrompt(); 95 | break; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/model/Certificate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.model; 6 | 7 | import org.linktechtips.constants.GlobalConstants; 8 | import org.linktechtips.process.config.ConfigEntry; 9 | import org.linktechtips.process.config.ConfigGroup; 10 | import org.linktechtips.support.Support; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import org.mindrot.jbcrypt.BCrypt; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Objects; 18 | 19 | import static org.linktechtips.Main.configManager; 20 | 21 | public class Certificate { 22 | public static final List certs = new ArrayList<>(); 23 | 24 | private String cid, password, origin; 25 | 26 | private int level, liveCheck; 27 | 28 | private long prevVisit, creation; 29 | private String passwordhash; 30 | private void configure() { 31 | ConfigEntry entry; 32 | ConfigGroup system = configManager.getGroup("system"); 33 | if (system != null) { 34 | if ((entry = system.getEntry("passwordhash")) != null) { 35 | passwordhash = entry.getData(); 36 | } 37 | } 38 | } 39 | public Certificate(String c, String p, int l, long crea, String o) { 40 | certs.add(this); 41 | liveCheck = 1; 42 | cid = c; 43 | configure(); 44 | if (Objects.equals(passwordhash, "true")) { 45 | password = BCrypt.hashpw(p, BCrypt.gensalt()); 46 | } else { 47 | password = p; 48 | } 49 | level = l; 50 | creation = crea; 51 | origin = o; 52 | if (level > GlobalConstants.LEV_MAX) { 53 | level = GlobalConstants.LEV_MAX; 54 | } 55 | prevVisit = 0; 56 | } 57 | 58 | public void close() { 59 | certs.remove(this); 60 | } 61 | 62 | public void configure(@Nullable String pwd, int l, long c, @Nullable String o) { 63 | level = l; 64 | if (pwd != null) { 65 | password = pwd; 66 | } 67 | if (o != null) { 68 | origin = o; 69 | } 70 | creation = c; 71 | } 72 | 73 | public static int maxLevel(String id, String p, int[] max /* pass by ref */) { 74 | Certificate cert = getCert(id); 75 | if (cert == null) { 76 | max[0] = GlobalConstants.LEV_OBSPILOT; 77 | return 0; 78 | } 79 | if (cert.getPassword().equals(p)) { 80 | max[0] = cert.getLevel(); 81 | cert.setPrevVisit(Support.mtime()); 82 | return 1; 83 | } 84 | max[0] = GlobalConstants.LEV_OBSPILOT; 85 | return 0; 86 | } 87 | 88 | public static @Nullable Certificate getCert(String cid) { 89 | return certs.stream().filter(e -> e.getCid().equals(cid)).findFirst().orElse(null); 90 | } 91 | 92 | public String getCid() { 93 | return cid; 94 | } 95 | 96 | public void setCid(String cid) { 97 | this.cid = cid; 98 | } 99 | 100 | public String getPassword() { 101 | return password; 102 | } 103 | 104 | public void setPassword(String password) { 105 | this.password = password; 106 | } 107 | 108 | public String getOrigin() { 109 | return origin; 110 | } 111 | 112 | public void setOrigin(String origin) { 113 | this.origin = origin; 114 | } 115 | 116 | public int getLevel() { 117 | return level; 118 | } 119 | 120 | public void setLevel(int level) { 121 | this.level = level; 122 | } 123 | 124 | public int getLiveCheck() { 125 | return liveCheck; 126 | } 127 | 128 | public void setLiveCheck(int liveCheck) { 129 | this.liveCheck = liveCheck; 130 | } 131 | 132 | public long getPrevVisit() { 133 | return prevVisit; 134 | } 135 | 136 | public void setPrevVisit(long prevVisit) { 137 | this.prevVisit = prevVisit; 138 | } 139 | 140 | public long getCreation() { 141 | return creation; 142 | } 143 | 144 | public void setCreation(long creation) { 145 | this.creation = creation; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /fsd.conf: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Sample configuration file for FSD 3 | # 4 | 5 | ############################################################################### 6 | # The system group holds information about your server. 7 | # 8 | # clientport & serverport: 9 | # The ports where clients and servers will connect to 10 | # systemport: 11 | # The port where the system management services will be located 12 | # ident: 13 | # The ident of your server. This ident has to be unique. It is used to 14 | # identify your server in the global network. It should not contain spaces. 15 | # Please use a small ident code, it will be sent in every packet 16 | # email: 17 | # The email address that can be used to mail the maintainer of this server. 18 | # name: 19 | # The name(description) of your server. It may contain spaces. 20 | # hostname: 21 | # The hostname that can be used to reach this server. 22 | # password: 23 | # The password you need to specify before you can execute privileged 24 | # commands on the system port. 25 | # location: 26 | # The (physical) location of the server in the world, and the internet. 27 | # Example: 'Delft, The netherlands (SURFnet)' 28 | # mode: 29 | # The mode of the server; can be 'normal' or 'silent'. Use 'normal' 30 | # for normal operation. 31 | # certificates: 32 | # The file to read certificates from. 33 | # maxclients: 34 | # The maximum amount of clients this server will allow 35 | # whazzup: 36 | # The file to put WhazzUp data in. 37 | 38 | [system] 39 | clientport=6809 40 | serverport=3011 41 | systemport=3010 42 | ident=FSD 43 | email=nobody@nowhere.com 44 | name=FSFDT FSD Unix Windows server 45 | hostname=localhost 46 | password=disable 47 | location=Nowhere 48 | mode=normal 49 | certificates=cert.txt 50 | mysqlmode=false 51 | #sqlurl=jdbc:mysql://主机名或IP地址:端口号/数据库名称 52 | #sqltable=BetterFSD 53 | #sqluser=41857486 54 | #sqlpassword=74867486 55 | passwordhash=false 56 | maxclients=200 57 | whazzup=whazzup.txt 58 | whazzupjson=whazzup.json 59 | webserverport=4185 60 | pilotcanbeobs=true 61 | 62 | ############################################################################### 63 | # The connections group holds information about the (server) connections this 64 | # server wil establish and accept. 65 | # 66 | # connectto: 67 | # Contains the hostname and port numbers of the servers to connect to. 68 | # Multiple servers can be used here. For example: 69 | # connectto=server.hinttech.com:5001,server.flightsim.com:4006 70 | # allowfrom: 71 | # Contains the IP addresses from which servers can connect to this server. 72 | # Multiple IP addresses can be used, separated by commas. For example: 73 | # allowfrom=server.flightsim.com,atc.aol.com 74 | 75 | [connections] 76 | #connectto= 77 | #allowfrom= 78 | 79 | ############################################################################### 80 | # The hosts group contains a list of hosts that are trusted for some activity. 81 | # There are 2 entries: 82 | # certificates : contains a list of server ID's that are allowed to change 83 | # certificates 84 | # weather : contains a list of server ID's that are allowed to change 85 | # weather profiles 86 | #[hosts] 87 | #certificates= 88 | #weather= 89 | 90 | ############################################################################### 91 | # This group controls the weather system. 92 | # The 'source' variable determines the source of the METAR data. 93 | # For normal operation, set this to 'network'. 94 | # There are 3 possible values here: 95 | # 'file' : Read the METAR data from the file 'metar.txt' 96 | # and allow weather requests from other servers. 97 | # 'download' : Like 'file', but refresh metar.txt every hour by downloading 98 | # the latest weather observations from metlab. The server has 99 | # to be connected to the internet for this to work. 100 | # 'network' : Relay weather requests to the closest METAR capable server. 101 | # 102 | # 'server','dir' and 'ftpmode' are only used when the METAR source is 'download'. These 103 | # fields determine the host name and the directory from where metar data is 104 | # read. FSD uses the FTP protocol to get the data. ftpmode can have the value 'active' 105 | # 'passive' that are Active and Passive FTP protocol mode, default is 'passive'. 106 | # If you use FSD on a computer having a private IP, only use passive mode. 107 | 108 | [weather] 109 | source=network 110 | server=tgftp.nws.noaa.gov 111 | dir=data/observations/metar/cycles/ 112 | ftpmode=passive 113 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/model/Flightplan.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.model; 6 | 7 | public class Flightplan { 8 | private String callsign; 9 | private int revision; 10 | private char type; 11 | private String aircraft; 12 | private int tasCruise; 13 | private String depAirport; 14 | private int depTime; 15 | private int actDepTime; 16 | private String alt; 17 | private String destAirport; 18 | private int hrsEnroute, minEnroute; 19 | private int hrsFuel, minFuel; 20 | private String altAirport; 21 | private String remarks; 22 | private String route; 23 | 24 | public Flightplan(String cs, char type, String aircraft, int tasCruise, 25 | String depAirport, int depTime, int actDepTime, String alt, 26 | String destAirport, int hrsEnroute, int minEnroute, int hrsFuel, 27 | int minFuel, String altAirport, String remarks, String route) { 28 | this.callsign = cs; 29 | this.type = type; 30 | this.aircraft = aircraft; 31 | this.tasCruise = tasCruise; 32 | this.depAirport = depAirport; 33 | this.depTime = depTime; 34 | this.actDepTime = actDepTime; 35 | this.alt = alt; 36 | this.destAirport = destAirport; 37 | this.hrsEnroute = hrsEnroute; 38 | this.minEnroute = minEnroute; 39 | this.hrsFuel = hrsFuel; 40 | this.minFuel = minFuel; 41 | this.altAirport = altAirport; 42 | this.remarks = remarks; 43 | this.route = route; 44 | } 45 | 46 | public String getCallsign() { 47 | return callsign; 48 | } 49 | 50 | public void setCallsign(String callsign) { 51 | this.callsign = callsign; 52 | } 53 | 54 | public int getRevision() { 55 | return revision; 56 | } 57 | 58 | public void setRevision(int revision) { 59 | this.revision = revision; 60 | } 61 | 62 | public char getType() { 63 | return type; 64 | } 65 | 66 | public void setType(char type) { 67 | this.type = type; 68 | } 69 | 70 | public String getAircraft() { 71 | return aircraft; 72 | } 73 | 74 | public void setAircraft(String aircraft) { 75 | this.aircraft = aircraft; 76 | } 77 | 78 | public int getTasCruise() { 79 | return tasCruise; 80 | } 81 | 82 | public void setTasCruise(int tasCruise) { 83 | this.tasCruise = tasCruise; 84 | } 85 | 86 | public String getDepAirport() { 87 | return depAirport; 88 | } 89 | 90 | public void setDepAirport(String depAirport) { 91 | this.depAirport = depAirport; 92 | } 93 | 94 | public int getDepTime() { 95 | return depTime; 96 | } 97 | 98 | public void setDepTime(int depTime) { 99 | this.depTime = depTime; 100 | } 101 | 102 | public int getActDepTime() { 103 | return actDepTime; 104 | } 105 | 106 | public void setActDepTime(int actDepTime) { 107 | this.actDepTime = actDepTime; 108 | } 109 | 110 | public String getAlt() { 111 | return alt; 112 | } 113 | 114 | public void setAlt(String alt) { 115 | this.alt = alt; 116 | } 117 | 118 | public String getDestAirport() { 119 | return destAirport; 120 | } 121 | 122 | public void setDestAirport(String destAirport) { 123 | this.destAirport = destAirport; 124 | } 125 | 126 | public int getHrsEnroute() { 127 | return hrsEnroute; 128 | } 129 | 130 | public void setHrsEnroute(int hrsEnroute) { 131 | this.hrsEnroute = hrsEnroute; 132 | } 133 | 134 | public int getMinEnroute() { 135 | return minEnroute; 136 | } 137 | 138 | public void setMinEnroute(int minEnroute) { 139 | this.minEnroute = minEnroute; 140 | } 141 | 142 | public int getHrsFuel() { 143 | return hrsFuel; 144 | } 145 | 146 | public void setHrsFuel(int hrsFuel) { 147 | this.hrsFuel = hrsFuel; 148 | } 149 | 150 | public int getMinFuel() { 151 | return minFuel; 152 | } 153 | 154 | public void setMinFuel(int minFuel) { 155 | this.minFuel = minFuel; 156 | } 157 | 158 | public String getAltAirport() { 159 | return altAirport; 160 | } 161 | 162 | public void setAltAirport(String altAirport) { 163 | this.altAirport = altAirport; 164 | } 165 | 166 | public String getRemarks() { 167 | return remarks; 168 | } 169 | 170 | public void setRemarks(String remarks) { 171 | this.remarks = remarks; 172 | } 173 | 174 | public String getRoute() { 175 | return route; 176 | } 177 | 178 | public void setRoute(String route) { 179 | this.route = route; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/config/ConfigManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.config; 6 | 7 | import org.linktechtips.constants.ManageVarType; 8 | import org.linktechtips.manager.Manage; 9 | import org.linktechtips.process.Process; 10 | import org.linktechtips.support.Support; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.io.*; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.nio.file.Paths; 21 | import java.nio.file.attribute.BasicFileAttributes; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.Objects; 25 | 26 | public class ConfigManager extends Process { 27 | private static final Logger LOGGER = LoggerFactory.getLogger(ConfigManager.class); 28 | 29 | private static final long CONFIG_INTERVAL = 10_000L; 30 | private final String fileName; 31 | 32 | private final @NotNull List groups; 33 | 34 | private final int varAccess; 35 | 36 | private boolean changed; 37 | 38 | private long prevCheck; 39 | 40 | private long lastModify; 41 | 42 | public ConfigManager(String name) { 43 | super(); 44 | fileName = name; 45 | groups = new ArrayList<>(); 46 | int nGroups = 0; 47 | changed = true; 48 | int fname = Manage.manager.addVar("config.filename", ManageVarType.ATT_VARCHAR); 49 | Manage.manager.setVar(fname, name); 50 | varAccess = Manage.manager.addVar("config.lastread", ManageVarType.ATT_DATE); 51 | parseFile(); 52 | } 53 | 54 | public @Nullable ConfigGroup getGroup(String name) { 55 | for (ConfigGroup group : groups) { 56 | if (Objects.equals(group.getName(), name)) { 57 | return group; 58 | } 59 | } 60 | 61 | return null; 62 | } 63 | 64 | public @NotNull ConfigGroup createGroup(String name) { 65 | ConfigGroup group = new ConfigGroup(name); 66 | groups.add(group); 67 | return group; 68 | } 69 | 70 | @Override 71 | public boolean run() { 72 | long now = Support.mtime(); 73 | if (now - prevCheck < CONFIG_INTERVAL) { 74 | return false; 75 | } 76 | prevCheck = now; 77 | Path file = Paths.get(fileName); 78 | if (Files.notExists(file)) { 79 | return false; 80 | } 81 | try { 82 | BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class); 83 | long fileLastModified = attr.lastModifiedTime().toMillis(); 84 | if (fileLastModified == lastModify) { 85 | return false; 86 | } 87 | lastModify = fileLastModified; 88 | parseFile(); 89 | return true; 90 | } catch (IOException e) { 91 | 92 | return false; 93 | } 94 | } 95 | 96 | public void parseFile() { 97 | File file = new File(fileName); 98 | if (!file.isFile() || !file.exists()) { 99 | return; 100 | } 101 | Manage.manager.setVar(varAccess, Support.mtime()); 102 | ConfigGroup current = null; 103 | try (InputStreamReader read = new InputStreamReader(new FileInputStream(file)); 104 | BufferedReader bufferedReader = new BufferedReader(read)) { 105 | String line; 106 | 107 | while ((line = bufferedReader.readLine()) != null) { 108 | if (StringUtils.isEmpty(line)) { 109 | continue; 110 | } 111 | if (line.charAt(0) == '#' || line.charAt(0) == '\r' || line.charAt(0) == '\n') { 112 | continue; 113 | } 114 | 115 | if (line.charAt(0) == '[') { 116 | String entry = line.substring(1, line.length() - 1); 117 | current = getGroup(entry); 118 | if (current == null) { 119 | current = createGroup(entry); 120 | } 121 | continue; 122 | } 123 | 124 | if (current == null) { 125 | continue; 126 | } 127 | 128 | String[] split = line.split("="); 129 | if (split.length > 1) { 130 | current.handleEntry(split[0], split[1]); 131 | } 132 | if (current.isChanged()) { 133 | changed = true; 134 | } 135 | } 136 | } catch (FileNotFoundException e) { 137 | LOGGER.error("Config file not found: " + fileName); 138 | } catch (IOException e) { 139 | LOGGER.error("Something went wrong when parse config file: ", e); 140 | } 141 | prevCheck = Support.mtime(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 4.0.0 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.7.5 11 | 12 | 13 | org.linktechtips 14 | betterfsd-java 15 | 1.1.2-hotfix1 16 | BetterFSD-Java 17 | Java refactoring version of BetterFSD 18 | https://github.com/LinkTechTips/BetterFSD-Java 19 | 20 | https://github.com/LinkTechTips/BetterFSD-Java/issues 21 | Github Issues 22 | 23 | 24 | 25 | MIT License 26 | https://github.com/LinkTechTips/BetterFSD-Java/blob/main/LICENSE 27 | repo 28 | 29 | 30 | 31 | https://github.com/LinkTechTips/BetterFSD-Java 32 | scm:git:git://github.com/LinkTechTips/BetterFSD-Java.git 33 | scm:git:git@github.com:LinkTechTips/BetterFSD-Java.git 34 | master 35 | 36 | 37 | 38 | 3174327625@qq.com 39 | Yuheng Wu 40 | https://github.com/LinkTechTips 41 | LinkTechTips 42 | 43 | 44 | 45 | 19 46 | 19 47 | UTF-8 48 | 49 | 50 | 51 | 52 | org.slf4j 53 | slf4j-api 54 | 1.7.25 55 | 56 | 57 | 58 | ch.qos.logback 59 | logback-classic 60 | 1.2.3 61 | 62 | 63 | org.apache.commons 64 | commons-lang3 65 | 3.12.0 66 | 67 | 68 | org.jetbrains 69 | annotations 70 | 23.0.0 71 | compile 72 | 73 | 74 | commons-io 75 | commons-io 76 | 2.11.0 77 | 78 | 79 | com.sun.net.httpserver 80 | http 81 | 20070405 82 | 83 | 84 | mysql 85 | mysql-connector-java 86 | 8.0.30 87 | 88 | 89 | org.mindrot 90 | jbcrypt 91 | 0.4 92 | 93 | 94 | 95 | compile 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-maven-plugin 100 | 2.7.5 101 | 102 | 103 | package 104 | 105 | repackage 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.linktechtips 116 | betterfsd-java 117 | ${project.version} 118 | 119 | 120 | 121 | 122 | 123 | 124 | github 125 | 126 | BetterFSD-Java 127 | https://maven.pkg.github.com/LinkTechTips/BetterFSD-Java 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/constants/ProtocolConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.constants; 6 | 7 | public class ProtocolConstants { 8 | 9 | public static final int CMD_NOTIFY = 0; 10 | public static final int CMD_REQ_METAR = 1; 11 | public static final int CMD_PING = 2; 12 | public static final int CMD_PONG = 3; 13 | public static final int CMD_SYNC = 4; 14 | public static final int CMD_LINK_DOWN = 5; 15 | public static final int CMD_NO_WX = 6; 16 | public static final int CMD_ADD_CLIENT = 7; 17 | public static final int CMD_RM_CLIENT = 8; 18 | public static final int CMD_PLAN = 9; 19 | public static final int CMD_PD = 10; 20 | public static final int CMD_AD = 11; 21 | public static final int CMD_CERT = 12; 22 | public static final int CMD_MULTICAST = 13; 23 | public static final int CMD_WEATHER = 14; 24 | public static final int CMD_METAR = 15; 25 | public static final int CMD_ADD_WPROFILE = 16; 26 | public static final int CMD_DEL_WPROFILE = 17; 27 | public static final int CMD_KILL = 18; 28 | public static final int CMD_RESET = 19; 29 | 30 | public static final int CL_ADDATC = 0; 31 | public static final int CL_RMATC = 1; 32 | public static final int CL_ADDPILOT = 2; 33 | public static final int CL_RMPILOT = 3; 34 | public static final int CL_REQHANDOFF = 4; 35 | public static final int CL_MESSAGE = 5; 36 | public static final int CL_REQWEATHER = 6; 37 | public static final int CL_PILOTPOS = 7; 38 | public static final int CL_ATCPOS = 8; 39 | public static final int CL_PING = 9; 40 | public static final int CL_PONG = 10; 41 | public static final int CL_ACHANDOFF = 11; 42 | public static final int CL_PLAN = 12; 43 | public static final int CL_SB = 13; 44 | public static final int CL_PC = 14; 45 | public static final int CL_WEATHER = 15; 46 | public static final int CL_CLOUDDATA = 16; 47 | public static final int CL_WINDDATA = 17; 48 | public static final int CL_TEMPDATA = 18; 49 | public static final int CL_REQCOM = 19; 50 | public static final int CL_REPCOM = 20; 51 | public static final int CL_REQACARS = 21; 52 | public static final int CL_REPACARS = 22; 53 | public static final int CL_ERROR = 23; 54 | public static final int CL_CQ = 24; 55 | public static final int CL_CR = 25; 56 | public static final int CL_KILL = 26; 57 | public static final int CL_WDELTA = 27; 58 | 59 | public static final int CL_MAX = 27; 60 | 61 | public static final int CERT_ADD = 0; 62 | public static final int CERT_DELETE = 1; 63 | public static final int CERT_MODIFY = 2; 64 | 65 | public static final int ERR_OK = 0; /* No error */ 66 | public static final int ERR_CSINUSE = 1; /* Callsign in use */ 67 | public static final int ERR_CSINVALID = 2; /* Callsign invalid */ 68 | public static final int ERR_REGISTERED = 3; /* Already registered */ 69 | public static final int ERR_SYNTAX = 4; /* Syntax error */ 70 | public static final int ERR_SRCINVALID = 5; /* Invalid source in packet */ 71 | public static final int ERR_CIDINVALID = 6; /* Invalid CID/password */ 72 | public static final int ERR_NOSUCHCS = 7; /* No such callsign */ 73 | public static final int ERR_NOFP = 8; /* No flightplan */ 74 | public static final int ERR_NOWEATHER = 9; /* No such weather profile */ 75 | public static final int ERR_REVISION = 10; /* Invalid protocol revision */ 76 | public static final int ERR_LEVEL = 11; /* Requested level too high */ 77 | public static final int ERR_SERVFULL = 12; /* No more clients */ 78 | public static final int ERR_CSSUSPEND = 13; /* CID/PID suspended */ 79 | public static final int ERR_PILOTASOBS = 14; /* cannot log in as observer */ 80 | 81 | 82 | public static final String[] CMD_NAMES = { 83 | "NOTIFY", 84 | "REQMETAR", 85 | "PING", 86 | "PONG", 87 | "SYNC", 88 | "LINKDOWN", 89 | "NOWX", 90 | "ADDCLIENT", 91 | "RMCLIENT", 92 | "PLAN", 93 | "PD", /* Pilot data */ 94 | "AD", /* ATC data */ 95 | "ADDCERT", 96 | "MC", 97 | "WX", 98 | "METAR", 99 | "AWPROF", 100 | "DWPROF", 101 | "KILL", 102 | "RESET" 103 | }; 104 | 105 | public static final int[] SILENT_OK = { 106 | 1, /* notify */ 107 | 1, /* reqmetar */ 108 | 1, /* ping */ 109 | 1, /* pong */ 110 | 1, /* sync */ 111 | 1, /* linkdown */ 112 | 1, /* nowx */ 113 | 1, /* addclient */ 114 | 1, /* rmclient */ 115 | 1, /* plan */ 116 | 0, /* pd */ 117 | 0, /* ad */ 118 | 1, /* addcert */ 119 | 0, /* mc */ 120 | 1, /* wx */ 121 | 1, /* metar */ 122 | 1, /* add w profile */ 123 | 1, /* del w profile */ 124 | 1, /* kill client */ 125 | 1 /* reset */ 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/support/Support.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.support; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.net.InetAddress; 12 | import java.net.UnknownHostException; 13 | import java.time.Clock; 14 | import java.time.Instant; 15 | import java.time.LocalDateTime; 16 | import java.time.ZoneId; 17 | import java.time.format.DateTimeFormatter; 18 | import java.util.List; 19 | 20 | public class Support { 21 | 22 | public static @NotNull String catCommand(@NotNull List array, int n, @NotNull StringBuilder buf) { 23 | if (array.size() == 0) { 24 | return ""; 25 | } 26 | for (int x = 0; x < n; x++) { 27 | if (x > 0) { 28 | buf.append(":"); 29 | } 30 | buf.append(array.get(x)); 31 | } 32 | return buf.toString(); 33 | } 34 | 35 | public static @NotNull String catArgs(@NotNull List array, int n, @NotNull StringBuilder buf) { 36 | for (int x = 0; x < n; x++) { 37 | if (x > 0) { 38 | buf.append(" "); 39 | } 40 | buf.append(array.get(x)); 41 | } 42 | return buf.toString(); 43 | } 44 | 45 | /* Takes bytes from the packet s and puts it into buf */ 46 | public static @NotNull String snapPacket(@NotNull String s, String buf, int bytes) { 47 | return buf + s.substring(0, bytes); 48 | } 49 | 50 | public static int breakPacket(@NotNull String s, @NotNull List arr, int max) { 51 | String[] split = s.split(":", -1); 52 | for (String str : split) { 53 | arr.add(StringUtils.defaultString(str, "")); 54 | if (arr.size() == max) { 55 | return arr.size(); 56 | } 57 | } 58 | 59 | return arr.size(); 60 | } 61 | 62 | public static int breakArgs(@NotNull String s, @NotNull List arr, int max) { 63 | String[] split = s.split(" "); 64 | for (String str : split) { 65 | str = StringUtils.strip(str); 66 | if (StringUtils.isNotBlank(str)) { 67 | arr.add(str); 68 | } 69 | if (arr.size() == max) { 70 | return arr.size(); 71 | } 72 | } 73 | 74 | return arr.size(); 75 | } 76 | 77 | public static String findHostname(String ip) { 78 | try { 79 | InetAddress address = InetAddress.getByName(ip); 80 | return address.getHostName(); 81 | } catch (UnknownHostException e) { 82 | return ip; 83 | } 84 | } 85 | 86 | public static double dist(double lat1, double lon1, double lat2, double lon2) { 87 | double dist, dlon = lon2 - lon1; 88 | lat1 *= Math.PI / 180.0; 89 | lat2 *= Math.PI / 180.0; 90 | dlon *= Math.PI / 180.0; 91 | dist = (Math.sin(lat1) * Math.sin(lat2)) + (Math.cos(lat1) * Math.cos(lat2) * Math.cos(dlon)); 92 | if (dist > 1.0) dist = 1.0; 93 | dist = Math.acos(dist) * 60 * 180 / Math.PI; 94 | return dist; 95 | } 96 | 97 | public static String printPart(double part, char p1, char p2) { 98 | char c = part < 0.0 ? p2 : p1; 99 | part = Math.abs(part); 100 | double degrees = Math.floor(part), min, sec; 101 | part -= degrees; 102 | part *= 60; 103 | min = Math.floor(part); 104 | part -= min; 105 | part *= 60; 106 | sec = part; 107 | 108 | return String.format("%c %02d %02d' %02d\"", c, (int) degrees, (int) min, (int) sec); 109 | } 110 | 111 | public static String printLoc(double lat, double lon) { 112 | String north = printPart(lat, 'N', 'S'); 113 | String east = printPart(lon, 'E', 'W'); 114 | return String.format("%s %s", north, east); 115 | } 116 | 117 | public static void startTimer() { 118 | 119 | } 120 | 121 | public static long mtime() { 122 | return Instant.now(Clock.systemDefaultZone()).toEpochMilli(); 123 | } 124 | 125 | public static long mgmtime() { 126 | return Instant.now(Clock.systemUTC()).toEpochMilli(); 127 | } 128 | 129 | public static @NotNull String sprintTime(long now) { 130 | return LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm:ss")); 131 | } 132 | 133 | public static @NotNull String sprintDate(long now) { 134 | if (now == 0) { 135 | return ""; 136 | } 137 | return LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss")); 138 | } 139 | 140 | public static @NotNull String sprintGmtDate(long now) { 141 | return LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of("UTC")).format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")); 142 | } 143 | 144 | public static @NotNull String sprintGmt(long now) { 145 | return LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.of("UTC")).format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/model/Server.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.model; 6 | 7 | import org.linktechtips.Main; 8 | import org.linktechtips.user.AbstractUser; 9 | import org.linktechtips.support.Support; 10 | import org.linktechtips.user.ServerUser; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Scanner; 19 | 20 | public class Server { 21 | public static Server myServer; 22 | 23 | public static final List servers = new ArrayList<>(); 24 | 25 | private final static Logger LOGGER = LoggerFactory.getLogger(Server.class); 26 | 27 | private int pCount; 28 | 29 | private int hops; 30 | 31 | private int check; 32 | 33 | private long lag; 34 | 35 | private int flags; 36 | 37 | private int packetDrops; 38 | 39 | private long alive; 40 | 41 | private String name; 42 | 43 | private String email; 44 | 45 | private String ident; 46 | 47 | private String hostName; 48 | 49 | private String version; 50 | 51 | private String location; 52 | 53 | private AbstractUser path; 54 | 55 | public Server(String ident, String name, String email, String hostName, String version, int flags, String location) { 56 | this.ident = ident; 57 | this.name = name; 58 | this.email = email; 59 | this.hostName = hostName; 60 | this.version = version; 61 | this.flags = flags; 62 | this.location = location; 63 | packetDrops = 0; 64 | hops = -1; 65 | pCount = -1; 66 | lag = -1; 67 | alive = Support.mtime(); 68 | } 69 | 70 | public void configure(String name, String email, String hostName, String version, String location) { 71 | this.name = name; 72 | this.email = email; 73 | this.hostName = hostName; 74 | this.version = version; 75 | this.location = location; 76 | } 77 | 78 | public void close() { 79 | LOGGER.info(String.format("[BetterFSD/ServerInterface]: Dropping server %s(%s)", ident, name)); 80 | for (Client client : Client.clients) { 81 | if (client.getLocation() == this) { 82 | client.close(); 83 | } 84 | } 85 | 86 | List users = Main.serverInterface.getUsers(); 87 | for (AbstractUser user : users) { 88 | if (((ServerUser) user).getThisServer() == this) { 89 | ((ServerUser) user).setThisServer(null); 90 | } 91 | } 92 | } 93 | 94 | public static @Nullable Server getServer(String ident) { 95 | for (Server server : servers) { 96 | if (server.getIdent().equals(ident)) { 97 | return server; 98 | } 99 | } 100 | return null; 101 | } 102 | 103 | public void setPath(AbstractUser who, int hops) { 104 | path = who; 105 | this.hops = hops; 106 | if (path == null) { 107 | pCount = -1; 108 | } 109 | } 110 | 111 | public void setAlive() { 112 | alive = Support.mtime(); 113 | } 114 | 115 | public void receivePong(@NotNull String data) { 116 | Scanner scanner = new Scanner(data); 117 | if (scanner.hasNextInt()) { 118 | scanner.nextInt(); 119 | if (scanner.hasNextLong()) { 120 | long t = scanner.nextLong(); 121 | lag = Support.mtime() - t; 122 | } 123 | } 124 | } 125 | 126 | public void clearServerChecks() { 127 | servers.forEach(s -> s.setCheck(0)); 128 | } 129 | 130 | public int getpCount() { 131 | return pCount; 132 | } 133 | 134 | public void setpCount(int pCount) { 135 | this.pCount = pCount; 136 | } 137 | 138 | public int getHops() { 139 | return hops; 140 | } 141 | 142 | public void setHops(int hops) { 143 | this.hops = hops; 144 | } 145 | 146 | public int getCheck() { 147 | return check; 148 | } 149 | 150 | public void setCheck(int check) { 151 | this.check = check; 152 | } 153 | 154 | public long getLag() { 155 | return lag; 156 | } 157 | 158 | public void setLag(long lag) { 159 | this.lag = lag; 160 | } 161 | 162 | public int getFlags() { 163 | return flags; 164 | } 165 | 166 | public void setFlags(int flags) { 167 | this.flags = flags; 168 | } 169 | 170 | public int getPacketDrops() { 171 | return packetDrops; 172 | } 173 | 174 | public void setPacketDrops(int packetDrops) { 175 | this.packetDrops = packetDrops; 176 | } 177 | 178 | public long getAlive() { 179 | return alive; 180 | } 181 | 182 | public void setAlive(long alive) { 183 | this.alive = alive; 184 | } 185 | 186 | public String getName() { 187 | return name; 188 | } 189 | 190 | public void setName(String name) { 191 | this.name = name; 192 | } 193 | 194 | public String getEmail() { 195 | return email; 196 | } 197 | 198 | public void setEmail(String email) { 199 | this.email = email; 200 | } 201 | 202 | public String getIdent() { 203 | return ident; 204 | } 205 | 206 | public void setIdent(String ident) { 207 | this.ident = ident; 208 | } 209 | 210 | public String getHostName() { 211 | return hostName; 212 | } 213 | 214 | public void setHostName(String hostName) { 215 | this.hostName = hostName; 216 | } 217 | 218 | public String getVersion() { 219 | return version; 220 | } 221 | 222 | public void setVersion(String version) { 223 | this.version = version; 224 | } 225 | 226 | public String getLocation() { 227 | return location; 228 | } 229 | 230 | public void setLocation(String location) { 231 | this.location = location; 232 | } 233 | 234 | public AbstractUser getPath() { 235 | return path; 236 | } 237 | 238 | public void setPath(AbstractUser path) { 239 | this.path = path; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/model/Client.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.model; 6 | 7 | import org.linktechtips.Main; 8 | import org.linktechtips.constants.ClientConstants; 9 | import org.linktechtips.support.Support; 10 | import org.apache.commons.lang3.math.NumberUtils; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | public class Client { 20 | 21 | public static final List clients = new ArrayList<>(); 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(Client.class); 24 | 25 | private long startTime; 26 | 27 | private Flightplan plan; 28 | 29 | private int type; 30 | 31 | private int rating; 32 | 33 | private long pbh; 34 | 35 | private int flags; 36 | 37 | private long alive; 38 | 39 | private String cid, callsign, protocol, realName, sector, identFlag; 40 | 41 | private double lat, lon; 42 | 43 | private int transponder, altitude, groundSpeed, frequency, facilityType; 44 | 45 | private int positionOk, visualRange, simType; 46 | 47 | private Server location; 48 | 49 | public Client(String i, Server where, String cs, int t, int reqRating, String rev, String real, int st) { 50 | clients.add(this); 51 | cid = i; 52 | location = where; 53 | callsign = cs; 54 | type = t; 55 | protocol = rev; 56 | rating = reqRating; 57 | visualRange = 40; 58 | realName = real; 59 | simType = st; 60 | startTime = alive = Support.mtime(); 61 | LOGGER.info(String.format("[User/Client]: %s as %s logged in", i, cs)); 62 | } 63 | 64 | public void close() { 65 | Main.serverInterface.clientDropped(this); 66 | clients.remove(this); 67 | } 68 | 69 | public void handleFp(String @NotNull [] array) { 70 | int revision = plan != null ? plan.getRevision() + 1 : 0; 71 | plan = new Flightplan(callsign, array[0].charAt(0), array[1], 72 | NumberUtils.toInt(array[2]), array[3], NumberUtils.toInt(array[4]), 73 | NumberUtils.toInt(array[5]), array[6], array[7], NumberUtils.toInt(array[8]), 74 | NumberUtils.toInt(array[9]), NumberUtils.toInt(array[10]), NumberUtils.toInt(array[11]), array[12], 75 | array[13], array[14]); 76 | plan.setRevision(revision); 77 | } 78 | 79 | /* Update the client fields, given a packet array */ 80 | public void updatePilot(String @NotNull [] array) { 81 | transponder = NumberUtils.toInt(array[2]); 82 | identFlag = array[0]; 83 | lat = NumberUtils.toDouble(array[4]); 84 | lon = NumberUtils.toDouble(array[5]); 85 | if (lat > 90.0 || lat < -90.0 || lon > 180.0 || lon < -180.0) { 86 | LOGGER.debug(String.format("POSERR: s=(%s,%s) got(%f,%f)", array[4], array[5], lat, lon)); 87 | } 88 | 89 | altitude = NumberUtils.toInt(array[6]); 90 | groundSpeed = NumberUtils.toInt(array[7]); 91 | pbh = NumberUtils.toLong(array[8]); 92 | 93 | flags = NumberUtils.toInt(array[9]); 94 | setAlive(); 95 | positionOk = 1; 96 | } 97 | 98 | public void updateAtc(String @NotNull [] array) { 99 | frequency = NumberUtils.toInt(array[0]); 100 | facilityType = NumberUtils.toInt(array[1]); 101 | visualRange = NumberUtils.toInt(array[2]); 102 | lat = NumberUtils.toDouble(array[4]); 103 | lon = NumberUtils.toDouble(array[5]); 104 | altitude = NumberUtils.toInt(array[6]); 105 | setAlive(); 106 | positionOk = 1; 107 | } 108 | 109 | public void setAlive() { 110 | alive = Support.mtime(); 111 | } 112 | 113 | public double distance(@Nullable Client other) { 114 | if (other == null) { 115 | return -1; 116 | } 117 | if (positionOk == 0 || other.getPositionOk() == 0) { 118 | return -1; 119 | } 120 | return Support.dist(lat, lon, other.lat, other.lon); 121 | } 122 | 123 | public int getRange() { 124 | if (type == ClientConstants.CLIENT_PILOT) { 125 | if (altitude < 0) { 126 | altitude = 0; 127 | } 128 | return (int) (10 + 1.414 * Math.sqrt(altitude)); 129 | } 130 | 131 | return switch (facilityType) { 132 | case 0 -> 40; /* Unknown */ 133 | case 1 -> 1500; /* FSS */ 134 | case 2 -> 5; /* CLR_DEL */ 135 | case 3 -> 5; /* GROUND */ 136 | case 4 -> 30; /* TOWER */ 137 | case 5 -> 100; /* APP/DEP */ 138 | case 6 -> 400; /* CENTER */ 139 | case 7 -> 1500; /* MONITOR */ 140 | default -> 40; 141 | }; 142 | } 143 | 144 | public static @Nullable Client getClient(String ident) { 145 | for (Client temp : clients) { 146 | if (temp.getCallsign().equals(ident)) { 147 | return temp; 148 | } 149 | } 150 | return null; 151 | } 152 | 153 | public long getStartTime() { 154 | return startTime; 155 | } 156 | 157 | public void setStartTime(long startTime) { 158 | this.startTime = startTime; 159 | } 160 | 161 | public Flightplan getPlan() { 162 | return plan; 163 | } 164 | 165 | public void setPlan(Flightplan plan) { 166 | this.plan = plan; 167 | } 168 | 169 | public int getType() { 170 | return type; 171 | } 172 | 173 | public void setType(int type) { 174 | this.type = type; 175 | } 176 | 177 | public int getRating() { 178 | return rating; 179 | } 180 | 181 | public void setRating(int rating) { 182 | this.rating = rating; 183 | } 184 | 185 | public long getPbh() { 186 | return pbh; 187 | } 188 | public void setPbh(long pbh) { 189 | this.pbh = pbh; 190 | } 191 | 192 | public int getFlags() { 193 | return flags; 194 | } 195 | 196 | public void setFlags(int flags) { 197 | this.flags = flags; 198 | } 199 | 200 | public long getAlive() { 201 | return alive; 202 | } 203 | 204 | public void setAlive(long alive) { 205 | this.alive = alive; 206 | } 207 | 208 | public String getCid() { 209 | return cid; 210 | } 211 | 212 | public void setCid(String cid) { 213 | this.cid = cid; 214 | } 215 | 216 | public String getCallsign() { 217 | return callsign; 218 | } 219 | 220 | public void setCallsign(String callsign) { 221 | this.callsign = callsign; 222 | } 223 | 224 | public String getProtocol() { 225 | return protocol; 226 | } 227 | 228 | public void setProtocol(String protocol) { 229 | this.protocol = protocol; 230 | } 231 | 232 | public String getRealName() { 233 | return realName; 234 | } 235 | 236 | public void setRealName(String realName) { 237 | this.realName = realName; 238 | } 239 | 240 | public String getSector() { 241 | return sector; 242 | } 243 | 244 | public void setSector(String sector) { 245 | this.sector = sector; 246 | } 247 | 248 | public String getIdentFlag() { 249 | return identFlag; 250 | } 251 | 252 | public void setIdentFlag(String identFlag) { 253 | this.identFlag = identFlag; 254 | } 255 | 256 | public double getLat() { 257 | return lat; 258 | } 259 | 260 | public void setLat(double lat) { 261 | this.lat = lat; 262 | } 263 | 264 | public double getLon() { 265 | return lon; 266 | } 267 | 268 | public void setLon(double lon) { 269 | this.lon = lon; 270 | } 271 | 272 | public int getTransponder() { 273 | return transponder; 274 | } 275 | 276 | public void setTransponder(int transponder) { 277 | this.transponder = transponder; 278 | } 279 | 280 | public int getAltitude() { 281 | return altitude; 282 | } 283 | 284 | public void setAltitude(int altitude) { 285 | this.altitude = altitude; 286 | } 287 | 288 | public int getGroundSpeed() { 289 | return groundSpeed; 290 | } 291 | 292 | public void setGroundSpeed(int groundSpeed) { 293 | this.groundSpeed = groundSpeed; 294 | } 295 | 296 | public int getFrequency() { 297 | return frequency; 298 | } 299 | 300 | public void setFrequency(int frequency) { 301 | this.frequency = frequency; 302 | } 303 | 304 | public int getFacilityType() { 305 | return facilityType; 306 | } 307 | 308 | public void setFacilityType(int facilityType) { 309 | this.facilityType = facilityType; 310 | } 311 | 312 | public int getPositionOk() { 313 | return positionOk; 314 | } 315 | 316 | public void setPositionOk(int positionOk) { 317 | this.positionOk = positionOk; 318 | } 319 | 320 | public int getVisualRange() { 321 | return visualRange; 322 | } 323 | 324 | public void setVisualRange(int visualRange) { 325 | this.visualRange = visualRange; 326 | } 327 | 328 | public int getSimType() { 329 | return simType; 330 | } 331 | 332 | public void setSimType(int simType) { 333 | this.simType = simType; 334 | } 335 | 336 | public Server getLocation() { 337 | return location; 338 | } 339 | 340 | public void setLocation(Server location) { 341 | this.location = location; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/network/ClientInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.network; 6 | 7 | import org.linktechtips.constants.ClientConstants; 8 | import org.linktechtips.constants.GlobalConstants; 9 | import org.linktechtips.constants.NetworkConstants; 10 | import org.linktechtips.constants.ProtocolConstants; 11 | import org.linktechtips.model.Client; 12 | import org.linktechtips.model.Flightplan; 13 | import org.linktechtips.model.Server; 14 | import org.linktechtips.user.AbstractUser; 15 | import org.linktechtips.user.ClientUser; 16 | import org.linktechtips.support.Support; 17 | import org.linktechtips.weather.CloudLayer; 18 | import org.linktechtips.weather.TempLayer; 19 | import org.linktechtips.weather.WProfile; 20 | import org.linktechtips.weather.WindLayer; 21 | import org.jetbrains.annotations.NotNull; 22 | import org.jetbrains.annotations.Nullable; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.nio.channels.SocketChannel; 27 | import java.util.Random; 28 | 29 | public class ClientInterface extends TcpInterface { 30 | private final static Logger LOGGER = LoggerFactory.getLogger(ClientInterface.class); 31 | 32 | private long prevWindDelta; 33 | 34 | public ClientInterface(int port, String code, String d) { 35 | super(port, code, d); 36 | prevWindDelta = Support.mtime(); 37 | } 38 | 39 | public boolean run() { 40 | boolean busy = super.run(); 41 | if ((Support.mtime() - prevWindDelta) > GlobalConstants.WIND_DELTA_TIMEOUT) { 42 | prevWindDelta = Support.mtime(); 43 | sendWindDelta(); 44 | } 45 | 46 | return busy; 47 | } 48 | 49 | public void newUser(SocketChannel fd, String peer, int portnum, int g) { 50 | insertUser(new ClientUser(fd, this, peer, portnum, g)); 51 | } 52 | 53 | public void sendAa(@NotNull Client who, AbstractUser ex) { 54 | 55 | String data = String.format("%s:SERVER:%s:%s::%d", who.getCallsign(), who.getRealName(), 56 | who.getCid(), who.getRating());//, who.getProtocol()); 57 | sendPacket(null, null, ex, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_ADDATC, data); 58 | } 59 | 60 | public void sendAp(@NotNull Client who, AbstractUser ex) { 61 | String data = String.format("%s:SERVER:%s::%d:%s:%d", who.getCallsign(), who.getCid(), who.getRating(), 62 | who.getProtocol(), who.getSimType()); 63 | sendPacket(null, null, ex, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_ADDPILOT, data); 64 | } 65 | 66 | public void sendDa(@NotNull Client who, AbstractUser ex) { 67 | 68 | String data = String.format("%s:%s", who.getCallsign(), who.getCid()); 69 | sendPacket(null, null, ex, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_RMATC, data); 70 | } 71 | 72 | public void sendDp(@NotNull Client who, AbstractUser ex) { 73 | 74 | String data = String.format("%s:%s", who.getCallsign(), who.getCid()); 75 | sendPacket(null, null, ex, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_RMPILOT, data); 76 | } 77 | 78 | public void sendWeather(@NotNull Client who, @NotNull WProfile p) { 79 | int x; 80 | StringBuilder buf = new StringBuilder(); 81 | String part; 82 | p.fix(who.getLat(), who.getLon()); 83 | buf = new StringBuilder(String.format("%s:%s", "server", who.getCallsign())); 84 | for (x = 0; x < 4; x++) { 85 | TempLayer l = p.getTemps().get(x); 86 | part = String.format(":%d:%d", l.getCeiling(), l.getTemp()); 87 | buf.append(part); 88 | } 89 | part = String.format(":%d", p.getBarometer()); 90 | buf.append(part); 91 | sendPacket(who, null, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_TEMPDATA, buf.toString()); 92 | 93 | buf = new StringBuilder(String.format("%s:%s", "server", who.getCallsign())); 94 | for (x = 0; x < 4; x++) { 95 | WindLayer l = p.getWinds().get(x); 96 | part = String.format(":%d:%d:%d:%d:%d:%d", l.getCeiling(), l.getFloor(), l.getDirection(), 97 | l.getSpeed(), l.getGusting(), l.getTurbulence()); 98 | buf.append(part); 99 | } 100 | sendPacket(who, null, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_WINDDATA, buf.toString()); 101 | 102 | buf = new StringBuilder(String.format("%s:%s", "server", who.getCallsign())); 103 | for (x = 0; x < 3; x++) { 104 | CloudLayer c = (x == 2 ? p.getTstorm() : p.getClouds().get(x)); 105 | part = String.format(":%d:%d:%d:%d:%d", c.getCeiling(), c.getFloor(), c.getCoverage(), 106 | c.getIcing(), c.getTurbulence()); 107 | buf.append(part); 108 | } 109 | part = String.format(":%.2f", p.getVisibility()); 110 | buf.append(part); 111 | sendPacket(who, null, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_CLOUDDATA, buf.toString()); 112 | } 113 | 114 | public void sendMetar(@NotNull Client who, String data) { 115 | String buf = String.format("server:%s:METAR:%s", who.getCallsign(), data); 116 | sendPacket(who, null, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_REPACARS, buf); 117 | } 118 | 119 | public void sendNoWx(Client who, String station) { 120 | for (AbstractUser temp : users) { 121 | ClientUser ctemp = (ClientUser) temp; 122 | if (ctemp.getThisClient() == who) { 123 | ctemp.showError(ProtocolConstants.ERR_NOWEATHER, station); 124 | break; 125 | } 126 | } 127 | } 128 | 129 | public int getBroad(String s) { 130 | int broad = ClientConstants.CLIENT_ALL; 131 | if ("*P".equals(s)) broad = ClientConstants.CLIENT_PILOT; 132 | else if ("*A".equals(s)) broad = ClientConstants.CLIENT_ATC; 133 | return broad; 134 | } 135 | 136 | public void sendGeneric(@NotNull String to, Client dest, AbstractUser ex, 137 | @Nullable Client source, String from, String s, int cmd) { 138 | int range = -1; 139 | String buf = String.format("%s:%s:%s", from, to, s); 140 | if (to.charAt(0) == '@' && source != null) 141 | range = source.getRange(); 142 | sendPacket(dest, source, ex, getBroad(to), range, cmd, buf); 143 | } 144 | 145 | public void sendPilotPos(@NotNull Client who, AbstractUser ex) { 146 | String data = String.format("%s:%s:%d:%d:%.5f:%.5f:%d:%d:%d:%d", who.getIdentFlag(), 147 | who.getCallsign(), who.getTransponder(), who.getRating(), who.getLat(), who.getLon(), 148 | who.getAltitude(), who.getGroundSpeed(), who.getPbh(), who.getFlags()); 149 | //dolog(L_INFO,"PBH unsigned value is %u",who.getpbh); 150 | //dolog(L_INFO,"SendPilotPos is sending: %s",data); 151 | sendPacket(null, who, ex, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_PILOTPOS, data); 152 | } 153 | 154 | public void sendAtcPos(@NotNull Client who, AbstractUser ex) { 155 | String data = String.format("%s:%d:%d:%d:%d:%.5f:%.5f:%d", who.getCallsign(), 156 | who.getFrequency(), who.getFacilityType(), who.getVisualRange(), who.getRating(), 157 | who.getLat(), who.getLon(), who.getAltitude()); 158 | sendPacket(null, who, ex, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_ATCPOS, data); 159 | } 160 | 161 | public void sendPlan(@Nullable Client dest, @NotNull Client who, int range) { 162 | String cs = (dest != null ? dest.getCallsign() : "*A"); 163 | Flightplan plan = who.getPlan(); 164 | String buf = String.format("%s:%s:%c:%s:%d:%s:%d:%d:%s:%s:%d:%d:%d:%d:%s:%s:%s", 165 | who.getCallsign(), cs, plan.getType(), plan.getAircraft(), 166 | plan.getTasCruise(), plan.getDepAirport(), plan.getDepTime(), plan.getActDepTime(), 167 | plan.getAlt(), plan.getDestAirport(), plan.getHrsEnroute(), plan.getMinEnroute(), 168 | plan.getHrsFuel(), plan.getMinFuel(), plan.getAltAirport(), plan.getRemarks(), 169 | plan.getRoute()); 170 | sendPacket(dest, null, null, ClientConstants.CLIENT_ATC, range, ProtocolConstants.CL_PLAN, buf); 171 | } 172 | 173 | public void handleKill(@NotNull Client who, String reason) { 174 | if (who.getLocation() != Server.myServer) return; 175 | for (AbstractUser temp : users) { 176 | ClientUser ctemp = (ClientUser) temp; 177 | if (ctemp.getThisClient() == who) { 178 | String buf = String.format("SERVER:%s:%s", who.getCallsign(), reason); 179 | sendPacket(who, null, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_KILL, buf); 180 | temp.kill(NetworkConstants.KILL_KILL); 181 | } 182 | } 183 | } 184 | 185 | public void sendWindDelta() { 186 | Random random = new Random(Support.mtime()); 187 | String buf; 188 | int speed = random.nextInt() % 11 - 5; 189 | int direction = random.nextInt() % 21 - 10; 190 | buf = String.format("SERVER:*:%d:%d", speed, direction); 191 | sendPacket(null, null, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_WDELTA, buf); 192 | } 193 | 194 | public int calcRange(@NotNull Client from, @NotNull Client to, int type, int range) { 195 | int x, y; 196 | switch (type) { 197 | case ProtocolConstants.CL_PILOTPOS: 198 | case ProtocolConstants.CL_ATCPOS: 199 | if (to.getType() == ClientConstants.CLIENT_ATC) return to.getVisualRange(); 200 | x = to.getRange(); 201 | y = from.getRange(); 202 | if (from.getType() == ClientConstants.CLIENT_PILOT) return x + y; 203 | return Math.max(x, y); 204 | case ProtocolConstants.CL_MESSAGE: 205 | x = to.getRange(); 206 | y = from.getRange(); 207 | if (from.getType() == ClientConstants.CLIENT_PILOT && to.getType() == ClientConstants.CLIENT_PILOT) 208 | return x + y; 209 | return Math.max(x, y); 210 | default: 211 | return range; 212 | } 213 | } 214 | 215 | /* Send a packet to a client. 216 | If is specified, only the client will receive the message. 217 | indicates if only pilot, only atc, or both will receive the 218 | message 219 | */ 220 | public void sendPacket(@Nullable Client dest, @Nullable Client source, AbstractUser exclude, 221 | int broad, int range, int cmd, String data) { 222 | if (dest != null) { 223 | if (dest.getLocation() != Server.myServer) return; 224 | } 225 | for (AbstractUser temp : users) 226 | if (temp.getKillFlag() == 0) { 227 | Client cl = ((ClientUser) temp).getThisClient(); 228 | if (cl == null) continue; 229 | if (exclude == temp) continue; 230 | if (dest != null && cl != dest) continue; 231 | if ((cl.getType() & broad) == 0) continue; 232 | if (source != null && (range != -1 || cmd == ProtocolConstants.CL_PILOTPOS || cmd == ProtocolConstants.CL_ATCPOS)) { 233 | int checkRange = calcRange(source, cl, cmd, range); 234 | double distance = cl.distance(source); 235 | if (distance == -1 || distance > checkRange) continue; 236 | } 237 | temp.uslprintf("%s%s\r\n", cmd == ProtocolConstants.CL_ATCPOS || cmd == ProtocolConstants.CL_PILOTPOS ? 1 : 0, 238 | ClientConstants.CL_CMD_NAMES[cmd], data); 239 | } 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/user/AbstractUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.user; 6 | 7 | import org.linktechtips.constants.GlobalConstants; 8 | import org.linktechtips.constants.NetworkConstants; 9 | import org.linktechtips.manager.Manage; 10 | import org.linktechtips.process.network.TcpInterface; 11 | import org.linktechtips.support.Support; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.IOException; 17 | import java.nio.ByteBuffer; 18 | import java.nio.channels.SelectableChannel; 19 | import java.nio.channels.SocketChannel; 20 | import java.util.Set; 21 | 22 | public class AbstractUser { 23 | private final static Logger LOGGER = LoggerFactory.getLogger(AbstractUser.class); 24 | 25 | private static int fdCount = 0; 26 | 27 | protected final int fd; 28 | 29 | protected final SocketChannel socketChannel; 30 | 31 | protected int killFlag, inSize, outSize, feed, feedCount, guardFlag; 32 | 33 | protected int outBufSoftLimit; 34 | 35 | protected long lastActive, lastPing, prevFeedCheck; 36 | 37 | protected int timeOut, blocked; 38 | 39 | protected ByteBuffer inBuf, outBuf; 40 | 41 | protected String peer; 42 | 43 | protected int port; 44 | 45 | protected String prompt; 46 | 47 | protected final TcpInterface baseParent; 48 | 49 | public AbstractUser(SocketChannel d, TcpInterface p, String peerName, int portNum, int g) { 50 | killFlag = 0; 51 | socketChannel = d; 52 | fd = ++fdCount; 53 | inBuf = ByteBuffer.allocate(1); 54 | outBuf = ByteBuffer.allocate(1); 55 | inSize = outSize = 1; 56 | blocked = 0; 57 | feedCount = 0; 58 | feed = -1; 59 | baseParent = p; 60 | timeOut = 0; 61 | peer = peerName; 62 | port = portNum; 63 | prevFeedCheck = 0; 64 | guardFlag = g; 65 | outBufSoftLimit = -1; 66 | setActive(); 67 | } 68 | 69 | public void close() { 70 | LOGGER.info(String.format("[User/AbstractUser]: client from %s removed from %s:%s", peer, baseParent.getDescription(), NetworkConstants.KILL_REASONS[killFlag])); 71 | Manage.manager.incVar(baseParent.getVarClosed()[killFlag]); 72 | if (killFlag != NetworkConstants.KILL_CLOSED && killFlag != NetworkConstants.KILL_WRITE_ERR) { 73 | output(); 74 | } 75 | if (killFlag != NetworkConstants.KILL_COMMAND && guardFlag > 0) { 76 | baseParent.addGuard(this); 77 | } 78 | try { 79 | socketChannel.close(); 80 | } catch (IOException e) { 81 | LOGGER.warn(String.format("[User/AbstractUser]: Close client %s failed.", peer), e); 82 | } 83 | } 84 | 85 | public void setActive() { 86 | lastActive = lastPing = Support.mtime(); 87 | } 88 | 89 | public void setMasks(@NotNull Set rmask, @NotNull Set wmask) { 90 | rmask.add(socketChannel); 91 | if (outBuf.position() > 0) { 92 | wmask.add(socketChannel); 93 | } 94 | } 95 | 96 | public void output() { 97 | int bytes; 98 | try { 99 | bytes = socketChannel.write(outBuf); 100 | byte[] data = new byte[bytes]; 101 | System.arraycopy(outBuf.array(), 0, data, 0, bytes); 102 | LOGGER.info("[User/AbstractUser]: Send: " + new String(data)); 103 | outBuf.clear(); 104 | } catch (IOException e) { 105 | kill(NetworkConstants.KILL_WRITE_ERR); 106 | return; 107 | } 108 | 109 | if ((baseParent.getFeedStrategy() & NetworkConstants.FEED_OUT) > 0) { 110 | feedCount += bytes; 111 | } 112 | } 113 | 114 | public void input() { 115 | int bytes; 116 | ByteBuffer buf; 117 | try { 118 | buf = ByteBuffer.allocate(1024); 119 | bytes = socketChannel.read(buf); 120 | if (bytes == -1) { 121 | kill(NetworkConstants.KILL_CLOSED); 122 | return; 123 | } 124 | } catch (IOException e) { 125 | kill(NetworkConstants.KILL_CLOSED); 126 | return; 127 | } 128 | 129 | if ((baseParent.getFeedStrategy() & NetworkConstants.FEED_IN) > 0) { 130 | feedCount += bytes; 131 | } 132 | 133 | int inBufBytes = inBuf.position(); 134 | 135 | if (inSize > 4096 && inBufBytes == 0) { 136 | inSize = 2048; 137 | inBuf.clear(); 138 | } 139 | 140 | buf.flip(); 141 | 142 | if (bytes + inBufBytes + 1 > inSize) { 143 | inSize = bytes + inBufBytes + 1000; 144 | ByteBuffer newInBuf = ByteBuffer.allocate(inSize); 145 | if (inBufBytes > 0) { 146 | inBuf.flip(); 147 | newInBuf.put(inBuf); 148 | inBuf.compact(); 149 | } 150 | newInBuf.put(buf); 151 | inBuf = newInBuf; 152 | } else { 153 | inBuf.put(buf); 154 | } 155 | buf.compact(); 156 | } 157 | 158 | private int nextLine(@NotNull StringBuilder dest) { 159 | if (!inBuf.hasArray()) { 160 | return -1; 161 | } 162 | byte[] data = new byte[inBuf.position()]; 163 | System.arraycopy(inBuf.array(), 0, data, 0, inBuf.position()); 164 | String src = new String(data); 165 | 166 | int len = src.indexOf("\r\n"); 167 | if (len == -1) { 168 | len = src.indexOf("\n"); 169 | } 170 | if (len == -1) { 171 | return -1; 172 | } 173 | 174 | if (len < GlobalConstants.MAX_LINE_LENGTH) { 175 | dest.append(src, 0, len); 176 | } 177 | 178 | ByteBuffer newInBuf = ByteBuffer.allocate(inSize); 179 | if (src.length() > len + 2) { 180 | newInBuf.put(src.substring(len + 2).getBytes()); 181 | } 182 | inBuf = newInBuf; 183 | 184 | if (len >= GlobalConstants.MAX_LINE_LENGTH) { 185 | return -1; 186 | } 187 | return len; 188 | } 189 | 190 | public void calcFeed() { 191 | long now = Support.mtime(); 192 | long elapsed = now - prevFeedCheck; 193 | double fact1 = elapsed / 300.0, fact2 = 1.0 - fact1; 194 | int newFeed = (int) (feedCount / elapsed); 195 | if (feed == -1) { 196 | feed = newFeed; 197 | } else { 198 | feed = (int) (newFeed * fact1 + feed * fact2); 199 | } 200 | feedCount = 0; 201 | prevFeedCheck = now; 202 | int bandWidth = feed; 203 | if (baseParent.getFeedStrategy() == NetworkConstants.FEED_BOTH) { 204 | bandWidth /= 2; 205 | } 206 | if (bandWidth > 50) { 207 | outBufSoftLimit = bandWidth * 30; 208 | } 209 | } 210 | 211 | private void send(@NotNull String buf) { 212 | byte[] bufBytes = buf.getBytes(); 213 | 214 | int outBufBytes = outBuf.position(); 215 | int bytes = bufBytes.length; 216 | 217 | if (outBufBytes > 0) { 218 | if (outBufBytes + bytes + 1 > outSize) { 219 | outSize = outBufBytes + bytes + 1000; 220 | ByteBuffer newOutBuf = ByteBuffer.allocate(outSize); 221 | outBuf.flip(); 222 | newOutBuf.put(outBuf); 223 | outBuf.compact(); 224 | newOutBuf.put(bufBytes); 225 | outBuf = newOutBuf; 226 | } else { 227 | outBuf.put(bufBytes); 228 | } 229 | } else { 230 | int sendBytes; 231 | try { 232 | ByteBuffer tmpBuf = ByteBuffer.wrap(bufBytes); 233 | sendBytes = socketChannel.write(tmpBuf); 234 | } catch (IOException e) { 235 | kill(NetworkConstants.KILL_WRITE_ERR); 236 | return; 237 | } 238 | 239 | if ((baseParent.getFeedStrategy() & NetworkConstants.FEED_OUT) > 0) { 240 | feedCount += sendBytes; 241 | } 242 | } 243 | 244 | } 245 | 246 | public void uprintf(@NotNull String format, Object... args) { 247 | if (killFlag != 0) { 248 | return; 249 | } 250 | 251 | String buf = String.format(format, args); 252 | send(buf); 253 | } 254 | 255 | public void uslprintf(@NotNull String format, int limit, Object... args) { 256 | if (killFlag != 0) { 257 | return; 258 | } 259 | 260 | String buf = String.format(format, args); 261 | if (limit > 0 && outBufSoftLimit != -1 && (buf.length() + outBuf.position()) > outBufSoftLimit) { 262 | return; 263 | } 264 | send(buf); 265 | } 266 | 267 | public int run() { 268 | int count = 0, stat, ok = 0; 269 | if (blocked == 1) { 270 | return 0; 271 | } 272 | StringBuilder bufBuilder = new StringBuilder(); 273 | while ((stat = nextLine(bufBuilder)) != -1 && (++count < 60)) { 274 | if (baseParent.getFloodLimit() != -1 && feed > baseParent.getFloodLimit()) { 275 | kill(NetworkConstants.KILL_FLOOD); 276 | } 277 | parse(bufBuilder.toString()); 278 | ok = 1; 279 | if (blocked == 1) { 280 | break; 281 | } 282 | bufBuilder.delete(0, bufBuilder.length()); 283 | } 284 | 285 | if (ok == 1 && stat == -1) { 286 | printPrompt(); 287 | } 288 | 289 | return stat == -1 ? 0 : 1; 290 | } 291 | 292 | public void parse(String s) { 293 | 294 | } 295 | 296 | public void block() { 297 | blocked = 1; 298 | } 299 | 300 | public void unblock() { 301 | blocked = 0; 302 | printPrompt(); 303 | } 304 | 305 | public void printPrompt() { 306 | if (prompt == null || killFlag > 0 || blocked > 0) { 307 | return; 308 | } 309 | 310 | uprintf("%s", prompt); 311 | } 312 | 313 | public void kill(int reason) { 314 | killFlag = reason; 315 | } 316 | 317 | public void sendPing() { 318 | 319 | } 320 | 321 | public int getFd() { 322 | return fd; 323 | } 324 | 325 | public SocketChannel getSocketChannel() { 326 | return socketChannel; 327 | } 328 | 329 | public int getKillFlag() { 330 | return killFlag; 331 | } 332 | 333 | public void setKillFlag(int killFlag) { 334 | this.killFlag = killFlag; 335 | } 336 | 337 | public int getInSize() { 338 | return inSize; 339 | } 340 | 341 | public void setInSize(int inSize) { 342 | this.inSize = inSize; 343 | } 344 | 345 | public int getOutSize() { 346 | return outSize; 347 | } 348 | 349 | public void setOutSize(int outSize) { 350 | this.outSize = outSize; 351 | } 352 | 353 | public int getFeed() { 354 | return feed; 355 | } 356 | 357 | public void setFeed(int feed) { 358 | this.feed = feed; 359 | } 360 | 361 | public int getFeedCount() { 362 | return feedCount; 363 | } 364 | 365 | public void setFeedCount(int feedCount) { 366 | this.feedCount = feedCount; 367 | } 368 | 369 | public int getGuardFlag() { 370 | return guardFlag; 371 | } 372 | 373 | public void setGuardFlag(int guardFlag) { 374 | this.guardFlag = guardFlag; 375 | } 376 | 377 | public int getOutBufSoftLimit() { 378 | return outBufSoftLimit; 379 | } 380 | 381 | public void setOutBufSoftLimit(int outBufSoftLimit) { 382 | this.outBufSoftLimit = outBufSoftLimit; 383 | } 384 | 385 | public long getLastActive() { 386 | return lastActive; 387 | } 388 | 389 | public void setLastActive(long lastActive) { 390 | this.lastActive = lastActive; 391 | } 392 | 393 | public long getLastPing() { 394 | return lastPing; 395 | } 396 | 397 | public void setLastPing(long lastPing) { 398 | this.lastPing = lastPing; 399 | } 400 | 401 | public long getPrevFeedCheck() { 402 | return prevFeedCheck; 403 | } 404 | 405 | public void setPrevFeedCheck(long prevFeedCheck) { 406 | this.prevFeedCheck = prevFeedCheck; 407 | } 408 | 409 | public int getTimeOut() { 410 | return timeOut; 411 | } 412 | 413 | public void setTimeOut(int timeOut) { 414 | this.timeOut = timeOut; 415 | } 416 | 417 | public int getBlocked() { 418 | return blocked; 419 | } 420 | 421 | public void setBlocked(int blocked) { 422 | this.blocked = blocked; 423 | } 424 | 425 | public String getPeer() { 426 | return peer; 427 | } 428 | 429 | public void setPeer(String peer) { 430 | this.peer = peer; 431 | } 432 | 433 | public int getPort() { 434 | return port; 435 | } 436 | 437 | public void setPort(int port) { 438 | this.port = port; 439 | } 440 | 441 | public String getPrompt() { 442 | return prompt; 443 | } 444 | 445 | public void setPrompt(String prompt) { 446 | this.prompt = prompt; 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/user/ClientUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.user; 6 | 7 | import org.linktechtips.Main; 8 | import org.linktechtips.manager.Manage; 9 | import org.linktechtips.model.Certificate; 10 | import org.linktechtips.model.Client; 11 | import org.linktechtips.model.Server; 12 | import org.linktechtips.process.config.ConfigEntry; 13 | import org.linktechtips.process.config.ConfigGroup; 14 | import org.linktechtips.process.metar.MetarManage; 15 | import org.linktechtips.process.network.ClientInterface; 16 | import org.linktechtips.process.network.TcpInterface; 17 | import org.linktechtips.support.Support; 18 | import org.linktechtips.constants.*; 19 | import org.apache.commons.lang3.ArrayUtils; 20 | import org.apache.commons.lang3.StringUtils; 21 | import org.apache.commons.lang3.math.NumberUtils; 22 | import org.jetbrains.annotations.NotNull; 23 | import org.jetbrains.annotations.Nullable; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import java.io.IOException; 28 | import java.nio.channels.SocketChannel; 29 | import java.nio.charset.StandardCharsets; 30 | import java.nio.file.Files; 31 | import java.nio.file.Paths; 32 | import java.util.ArrayList; 33 | import java.util.Arrays; 34 | import java.util.List; 35 | import java.util.Objects; 36 | 37 | import static org.linktechtips.Main.configManager; 38 | 39 | public class ClientUser extends AbstractUser { 40 | 41 | private static final Logger LOGGER = LoggerFactory.getLogger(ClientUser.class); 42 | 43 | private @Nullable Client thisClient; 44 | 45 | public ClientUser(SocketChannel channel, @NotNull ClientInterface p, String pn, int portnum, int gg) { 46 | super(channel, p, pn, portnum, gg); 47 | thisClient = null; 48 | ConfigGroup gu = configManager.getGroup("system"); 49 | ConfigEntry e = null; 50 | if (gu != null) { 51 | e = gu.getEntry("maxclients"); 52 | } 53 | int total = Objects.requireNonNull(Manage.manager.getVar(p.getVarCurrent())).getValue().getNumber(); 54 | if (e != null && NumberUtils.toInt(e.getData()) <= total) { 55 | showError(ProtocolConstants.ERR_SERVFULL, ""); 56 | kill(NetworkConstants.KILL_COMMAND); 57 | } 58 | } 59 | 60 | @Override 61 | public void close() { 62 | super.close(); 63 | if (thisClient != null) { 64 | int type = thisClient.getType(); 65 | String callsign = thisClient.getCallsign(); 66 | String cid = thisClient.getCid(); 67 | LOGGER.info(String.format("[User/ClientUser]: %s as %s logged out", cid, callsign)); 68 | Main.serverInterface.sendRmClient(null, "*", thisClient, this); 69 | thisClient.close(); 70 | } 71 | } 72 | 73 | public void readMotd() { 74 | String line = GlobalConstants.PRODUCT; 75 | Main.clientInterface.sendGeneric(Objects.requireNonNull(thisClient).getCallsign(), thisClient, null, 76 | null, "server", line, ProtocolConstants.CL_MESSAGE); 77 | 78 | List lines; 79 | try { 80 | lines = Files.readAllLines(Paths.get(FsdPath.PATH_FSD_MOTD), StandardCharsets.UTF_8); 81 | } catch (IOException e) { 82 | return; 83 | } 84 | 85 | for (String msg : lines) { 86 | Main.clientInterface.sendGeneric(thisClient.getCallsign(), thisClient, null, null, "server", msg, ProtocolConstants.CL_MESSAGE); 87 | } 88 | } 89 | 90 | public void parse(@NotNull String s) { 91 | setActive(); 92 | doParse(s); 93 | } 94 | 95 | /* Checks if the given callsign is OK. returns 0 on success or errorcode 96 | on failure */ 97 | public int callsignOk(@NotNull String name) { 98 | if (name.length() < 2 || name.length() > GlobalConstants.CALLSIGN_BYTES) return ProtocolConstants.ERR_CSINVALID; 99 | if (StringUtils.indexOfAny(name, "!@#$%*:& \t") != -1) return ProtocolConstants.ERR_CSINVALID; 100 | for (Client client : Client.clients) 101 | if (client.getCallsign().equals(name)) return ProtocolConstants.ERR_CSINUSE; 102 | return ProtocolConstants.ERR_OK; 103 | } 104 | 105 | public int checkSource(@NotNull String from) { 106 | if (!from.equalsIgnoreCase(Objects.requireNonNull(thisClient).getCallsign())) { 107 | showError(ProtocolConstants.ERR_SRCINVALID, from); 108 | return 0; 109 | } 110 | return 1; 111 | } 112 | 113 | public int getComm(@NotNull String cmd) { 114 | for (int index = 0; index < ClientConstants.CL_CMD_NAMES.length; index++) { 115 | if (cmd.startsWith(ClientConstants.CL_CMD_NAMES[index])) { 116 | return index; 117 | } 118 | } 119 | return -1; 120 | } 121 | 122 | public ClientUser(SocketChannel d, TcpInterface p, String peerName, int portNum, int g) { 123 | super(d, p, peerName, portNum, g); 124 | } 125 | 126 | public void showError(int num, String env) { 127 | uprintf("$ERserver:%s:%03d:%s:%s\r\n", thisClient != null ? thisClient.getCallsign() : 128 | "unknown", num, env, ClientConstants.ERR_STR[num]); 129 | } 130 | 131 | public int checkLogin(String id, String pwd, int req) { 132 | if (StringUtils.isEmpty(id)) return -2; 133 | int[] max = new int[1]; 134 | int ok = Certificate.maxLevel(id, pwd, max); 135 | if (ok == 0) { 136 | showError(ProtocolConstants.ERR_CIDINVALID, id); 137 | return -1; 138 | } 139 | return Math.min(req, max[0]); 140 | } 141 | private String pilotcanbeobs; 142 | private void configure() { 143 | ConfigEntry entry; 144 | ConfigGroup system = configManager.getGroup("system"); 145 | if (system != null) { 146 | if ((entry = system.getEntry("pilotcanbeobs")) != null) { 147 | pilotcanbeobs = entry.getData(); 148 | } 149 | } 150 | } 151 | public void execAa(String[] s, int count) { 152 | configure(); 153 | if (thisClient != null) { 154 | showError(ProtocolConstants.ERR_REGISTERED, ""); 155 | return; 156 | } 157 | if (count < 7) { 158 | showError(ProtocolConstants.ERR_SYNTAX, ""); 159 | return; 160 | } 161 | int err = callsignOk(s[0]); 162 | if (err != 0) { 163 | showError(err, ""); 164 | kill(NetworkConstants.KILL_COMMAND); 165 | return; 166 | } 167 | if (NumberUtils.toInt(s[6]) != GlobalConstants.NEED_REVISION) { 168 | showError(ProtocolConstants.ERR_REVISION, ""); 169 | kill(NetworkConstants.KILL_PROTOCOL); 170 | return; 171 | } 172 | int req = NumberUtils.toInt(s[5]); 173 | if (req < 0) req = 0; 174 | int level = checkLogin(s[3], s[4], req); 175 | if (Objects.equals(pilotcanbeobs, "false")) { 176 | if (level == 0) { 177 | showError(ProtocolConstants.ERR_CSSUSPEND, ""); 178 | kill(NetworkConstants.KILL_COMMAND); 179 | return; 180 | } else if(level == 1){ 181 | showError(ProtocolConstants.ERR_PILOTASOBS, ""); 182 | kill(NetworkConstants.KILL_COMMAND); 183 | return; 184 | } else if (level == -1) { 185 | kill(NetworkConstants.KILL_COMMAND); 186 | return; 187 | } else if (level == -2) level = 1; 188 | } else { 189 | if (level == 0) { 190 | showError(ProtocolConstants.ERR_CSSUSPEND, ""); 191 | kill(NetworkConstants.KILL_COMMAND); 192 | return; 193 | } else if (level == -1) { 194 | kill(NetworkConstants.KILL_COMMAND); 195 | return; 196 | } else if (level == -2) level = 1; 197 | } 198 | if (level < req) { 199 | showError(ProtocolConstants.ERR_LEVEL, s[5]); 200 | kill(NetworkConstants.KILL_COMMAND); 201 | return; 202 | } 203 | thisClient = new Client(s[3], Server.myServer, s[0], ClientConstants.CLIENT_ATC, level, s[6], s[2], 204 | -1); 205 | Main.serverInterface.sendAddClient("*", thisClient, null, this, 0); 206 | readMotd(); 207 | } 208 | 209 | public void execAp(String[] s, int count) { 210 | if (thisClient != null) { 211 | showError(ProtocolConstants.ERR_REGISTERED, ""); 212 | return; 213 | } 214 | if (count < 8) { 215 | showError(ProtocolConstants.ERR_SYNTAX, ""); 216 | return; 217 | } 218 | int err = callsignOk(s[0]); 219 | if (err != 0) { 220 | showError(err, ""); 221 | kill(NetworkConstants.KILL_COMMAND); 222 | return; 223 | } 224 | if (NumberUtils.toInt(s[5]) != GlobalConstants.NEED_REVISION) { 225 | showError(ProtocolConstants.ERR_REVISION, ""); 226 | kill(NetworkConstants.KILL_PROTOCOL); 227 | return; 228 | } 229 | int req = NumberUtils.toInt(s[4]); 230 | if (req < 0) req = 0; 231 | int level = checkLogin(s[2], s[3], req); 232 | if (level < 0) { 233 | kill(NetworkConstants.KILL_COMMAND); 234 | return; 235 | } else if (level == 0) { 236 | showError(ProtocolConstants.ERR_CSSUSPEND, ""); 237 | kill(NetworkConstants.KILL_COMMAND); 238 | return; 239 | } 240 | if (level < req) { 241 | showError(ProtocolConstants.ERR_LEVEL, s[4]); 242 | kill(NetworkConstants.KILL_COMMAND); 243 | return; 244 | } 245 | thisClient = new Client(s[2], Server.myServer, s[0], ClientConstants.CLIENT_PILOT, level, s[4], s[7], 246 | NumberUtils.toInt(s[6])); 247 | Main.serverInterface.sendAddClient("*", thisClient, null, this, 0); 248 | readMotd(); 249 | } 250 | 251 | public void execMulticast(String @NotNull [] s, int count, int cmd, int nargs, int multiok) { 252 | nargs += 2; 253 | if (count < nargs) { 254 | showError(ProtocolConstants.ERR_SYNTAX, ""); 255 | return; 256 | } 257 | String[] subarray = ArrayUtils.subarray(s, 2, s.length); 258 | String data = Support.catCommand(Arrays.asList(subarray), count - 2, new StringBuilder()); 259 | String from = s[0], to = s[1]; 260 | if (checkSource(from) == 0) return; 261 | Main.serverInterface.sendMulticast(thisClient, to, data, cmd, multiok, this); 262 | } 263 | 264 | public void exeCd(String[] s, int count) { 265 | if (count == 0) { 266 | showError(ProtocolConstants.ERR_SYNTAX, ""); 267 | return; 268 | } 269 | if (checkSource(s[0]) == 0) return; 270 | kill(NetworkConstants.KILL_COMMAND); 271 | } 272 | 273 | public void execPilotPos(String @NotNull [] array, int count) { 274 | if (count < 10) { 275 | showError(ProtocolConstants.ERR_SYNTAX, ""); 276 | return; 277 | } 278 | if (checkSource(array[1]) == 0) return; 279 | Objects.requireNonNull(thisClient).updatePilot(array); 280 | Main.serverInterface.sendPilotData(thisClient, this); 281 | } 282 | 283 | public void execAtcPos(String @NotNull [] array, int count) { 284 | if (count < 8) { 285 | showError(ProtocolConstants.ERR_SYNTAX, ""); 286 | return; 287 | } 288 | if (checkSource(array[0]) == 0) return; 289 | Objects.requireNonNull(thisClient).updateAtc(ArrayUtils.subarray(array, 1, array.length)); 290 | Main.serverInterface.sendAtcData(thisClient, this); 291 | } 292 | 293 | public void execFp(String @NotNull [] array, int count) { 294 | if (count < 17) { 295 | showError(ProtocolConstants.ERR_SYNTAX, ""); 296 | return; 297 | } 298 | if (checkSource(array[0]) == 0) return; 299 | Objects.requireNonNull(thisClient).handleFp(ArrayUtils.subarray(array, 2, array.length)); 300 | Main.serverInterface.sendPlan("*", thisClient, null); 301 | } 302 | 303 | public void execWeather(String[] array, int count) { 304 | if (count < 3) { 305 | showError(ProtocolConstants.ERR_SYNTAX, ""); 306 | return; 307 | } 308 | if (checkSource(array[0]) == 0) return; 309 | String source = String.format("%%%s", Objects.requireNonNull(thisClient).getCallsign()); 310 | MetarManage.metarManager.requestMetar(source, array[2], 1, -1); 311 | } 312 | 313 | public void execAcars(String[] array, int count) { 314 | if (count < 3) { 315 | showError(ProtocolConstants.ERR_SYNTAX, ""); 316 | return; 317 | } 318 | if (checkSource(array[0]) == 0) return; 319 | if ("METAR".equalsIgnoreCase(array[2]) && count > 3) { 320 | String source = String.format("%%%s", Objects.requireNonNull(thisClient).getCallsign()); 321 | MetarManage.metarManager.requestMetar(source, array[3], 0, -1); 322 | } 323 | } 324 | 325 | public void execCq(String @NotNull [] array, int count) { 326 | if (count < 3) { 327 | showError(ProtocolConstants.ERR_SYNTAX, ""); 328 | return; 329 | } 330 | if ("server".equalsIgnoreCase(array[1])) { 331 | execMulticast(array, count, ProtocolConstants.CL_CQ, 1, 1); 332 | return; 333 | } 334 | if ("RN".equalsIgnoreCase(array[2])) { 335 | Client cl = Client.getClient(array[1]); 336 | if (cl != null) { 337 | String data = String.format("%s:%s:RN:%s:USER:%d", cl.getCallsign(), Objects.requireNonNull(thisClient).getCallsign(), cl.getRealName(), cl.getRating()); 338 | Main.clientInterface.sendPacket(thisClient, cl, null, ClientConstants.CLIENT_ALL, -1, ProtocolConstants.CL_CR, data); 339 | return; 340 | } 341 | } 342 | if ("fp".equalsIgnoreCase(array[2])) { 343 | Client cl = Client.getClient(array[3]); 344 | if (cl == null) { 345 | showError(ProtocolConstants.ERR_NOSUCHCS, array[3]); 346 | return; 347 | } 348 | if (cl.getPlan() == null) { 349 | showError(ProtocolConstants.ERR_NOFP, ""); 350 | return; 351 | } 352 | Main.clientInterface.sendPlan(thisClient, cl, -1); 353 | } 354 | } 355 | 356 | public void execKill(String[] array, int count) { 357 | if (count < 3) { 358 | showError(ProtocolConstants.ERR_SYNTAX, ""); 359 | return; 360 | } 361 | Client cl = Client.getClient(array[1]); 362 | if (cl != null) { 363 | showError(ProtocolConstants.ERR_NOSUCHCS, array[1]); 364 | return; 365 | } 366 | 367 | String junk; 368 | 369 | if (Objects.requireNonNull(thisClient).getRating() < 11) { 370 | junk = "You are not allowed to kill users!"; 371 | Main.clientInterface.sendGeneric(thisClient.getCallsign(), thisClient, null, 372 | null, "server", junk, ProtocolConstants.CL_MESSAGE); 373 | junk = String.format("%s attempted to remove %s, but was not allowed to", thisClient.getCallsign(), array[1]); 374 | LOGGER.error("[User/ClientUser]: " + junk); 375 | } else { 376 | junk = String.format("Attempting to kill %s", array[1]); 377 | Main.clientInterface.sendGeneric(thisClient.getCallsign(), thisClient, null, 378 | null, "server", junk, ProtocolConstants.CL_MESSAGE); 379 | junk = String.format("%s Killed %s", thisClient.getCallsign(), array[1]); 380 | LOGGER.info("[User/ClientUser]: " + junk); 381 | Main.serverInterface.sendKill(cl, array[2]); 382 | } 383 | } 384 | 385 | void doParse(@NotNull String s) { 386 | String cmd = ""; 387 | List list = new ArrayList<>(); 388 | cmd = Support.snapPacket(s, cmd, 3); 389 | int index = getComm(cmd), count; 390 | if (index == -1) { 391 | showError(ProtocolConstants.ERR_SYNTAX, ""); 392 | return; 393 | } 394 | if (thisClient == null && index != ProtocolConstants.CL_ADDATC && index != ProtocolConstants.CL_ADDPILOT) 395 | return; 396 | 397 | /* Just a hack to put the pointer on the first arg here */ 398 | String str = s.substring(ClientConstants.CL_CMD_NAMES[index].length()); 399 | 400 | count = Support.breakPacket(str, list, 100); 401 | 402 | String[] array = list.toArray(new String[0]); 403 | switch (index) { 404 | case ProtocolConstants.CL_ADDATC -> execAa(array, count); 405 | case ProtocolConstants.CL_ADDPILOT -> execAp(array, count); 406 | case ProtocolConstants.CL_PLAN -> execFp(array, count); 407 | /* Handled like RMPILOT */ 408 | case ProtocolConstants.CL_RMATC, ProtocolConstants.CL_RMPILOT -> exeCd(array, count); 409 | case ProtocolConstants.CL_PILOTPOS -> execPilotPos(array, count); 410 | case ProtocolConstants.CL_ATCPOS -> execAtcPos(array, count); 411 | case ProtocolConstants.CL_PONG, ProtocolConstants.CL_PING -> execMulticast(array, count, index, 0, 1); 412 | case ProtocolConstants.CL_MESSAGE -> execMulticast(array, count, index, 1, 1); 413 | case ProtocolConstants.CL_REQHANDOFF, ProtocolConstants.CL_ACHANDOFF, ProtocolConstants.CL_REPCOM -> 414 | execMulticast(array, count, index, 1, 0); 415 | case ProtocolConstants.CL_SB, ProtocolConstants.CL_PC, ProtocolConstants.CL_REQCOM -> execMulticast(array, count, index, 0, 0); 416 | case ProtocolConstants.CL_WEATHER -> execWeather(array, count); 417 | case ProtocolConstants.CL_REQACARS -> execAcars(array, count); 418 | case ProtocolConstants.CL_CR -> execMulticast(array, count, index, 2, 0); 419 | case ProtocolConstants.CL_CQ -> execCq(array, count); 420 | case ProtocolConstants.CL_KILL -> execKill(array, count); 421 | default -> showError(ProtocolConstants.ERR_SYNTAX, ""); 422 | } 423 | } 424 | 425 | public @Nullable Client getThisClient() { 426 | return thisClient; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/network/TcpInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.network; 6 | 7 | import org.linktechtips.constants.GlobalConstants; 8 | import org.linktechtips.constants.ManageVarType; 9 | import org.linktechtips.constants.NetworkConstants; 10 | import org.linktechtips.manager.Manage; 11 | import org.linktechtips.process.Process; 12 | import org.linktechtips.support.Support; 13 | import org.linktechtips.user.AbstractUser; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.io.IOException; 20 | import java.net.*; 21 | import java.nio.ByteBuffer; 22 | import java.nio.channels.*; 23 | import java.util.*; 24 | 25 | import static java.net.StandardSocketOptions.TCP_NODELAY; 26 | 27 | public class TcpInterface extends Process { 28 | private static final Logger LOGGER = LoggerFactory.getLogger(TcpInterface.class); 29 | 30 | protected String description; 31 | 32 | protected ServerSocketChannel sock; 33 | 34 | private Selector selector; 35 | 36 | protected SelectionKey selectionKey; 37 | 38 | protected int varCurrent; 39 | 40 | protected int varTotal; 41 | 42 | protected int varPeak; 43 | 44 | protected int[] varClosed; 45 | 46 | protected int feedStrategy; 47 | 48 | protected int floodLimit; 49 | 50 | protected int outBufLimit; 51 | 52 | protected long prevChecks; 53 | 54 | protected @Nullable String prompt; 55 | 56 | protected List users; 57 | 58 | protected List guards; 59 | 60 | protected List allows; 61 | 62 | public TcpInterface(int port, String code, String desc) { 63 | super(); 64 | boolean on = true; 65 | description = desc; 66 | allows = new ArrayList<>(); 67 | varClosed = new int[9]; 68 | floodLimit = -1; 69 | outBufLimit = -1; 70 | makeVars(code); 71 | try { 72 | selector = Selector.open(); 73 | sock = ServerSocketChannel.open(); 74 | } catch (IOException e) { 75 | LOGGER.error("[Network/TcpInterface]: Could not open server socket channel.", e); 76 | System.exit(1); 77 | } 78 | try { 79 | sock.setOption(TCP_NODELAY, Boolean.TRUE); 80 | } catch (IOException e) { 81 | LOGGER.error(String.format("[Network/TcpInterface]: Could not set TCP_NODELAY on port %d", port), e); 82 | System.exit(1); 83 | } catch (UnsupportedOperationException e) { 84 | LOGGER.warn("[Network/TcpInterface]: TCP_NODELAY is not supported on current platform"); 85 | } 86 | try { 87 | sock.setOption(StandardSocketOptions.SO_REUSEADDR, on); 88 | } catch (IOException e) { 89 | LOGGER.error(String.format("[Network/TcpInterface]: setsockopt error on port %d", port), e); 90 | System.exit(1); 91 | } catch (UnsupportedOperationException e) { 92 | LOGGER.warn("[Network/TcpInterface]: SO_REUSEADDR is not supported on current platform"); 93 | } 94 | 95 | try { 96 | sock.configureBlocking(false); 97 | sock.bind(new InetSocketAddress(port)); 98 | sock.register(selector, SelectionKey.OP_ACCEPT); 99 | } catch (IOException e) { 100 | LOGGER.error(String.format("[Network/TcpInterface]: Bind error on port %d", port), e); 101 | System.exit(1); 102 | } 103 | 104 | users = new ArrayList<>(); 105 | guards = new ArrayList<>(); 106 | prompt = null; 107 | feedStrategy = 0; 108 | prevChecks = 0; 109 | LOGGER.info(String.format("[Network/TcpInterface]: Booting port %d (%s)", port, description)); 110 | } 111 | 112 | public void close() { 113 | try { 114 | sock.close(); 115 | } catch (IOException e) { 116 | LOGGER.warn("[Network/TcpInterface]: Close tcpInterface error."); 117 | } 118 | } 119 | 120 | public void makeVars(String code) { 121 | String varName; 122 | varName = String.format("interface.%s.current", code); 123 | varCurrent = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 124 | varName = String.format("interface.%s.total", code); 125 | varTotal = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 126 | varName = String.format("interface.%s.peak", code); 127 | varPeak = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 128 | 129 | varName = String.format("interface.%s.command", code); 130 | varClosed[1] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 131 | varName = String.format("interface.%s.flood", code); 132 | varClosed[2] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 133 | varName = String.format("interface.%s.initialtimeout", code); 134 | varClosed[3] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 135 | varName = String.format("interface.%s.stalled", code); 136 | varClosed[4] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 137 | varName = String.format("interface.%s.closed", code); 138 | varClosed[5] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 139 | varName = String.format("interface.%s.writeerr", code); 140 | varClosed[6] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 141 | varName = String.format("interface.%s.killed", code); 142 | varClosed[7] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 143 | varName = String.format("interface.%s.protocol", code); 144 | varClosed[8] = Manage.manager.addVar(varName, ManageVarType.ATT_INT); 145 | } 146 | 147 | public void addGuard(@NotNull AbstractUser who) { 148 | Guard guard = new Guard(); 149 | guard.setHost(who.getPeer()); 150 | guard.setPort(who.getPort()); 151 | guard.setPrevTry(Support.mtime()); 152 | } 153 | 154 | public void allow(String name) { 155 | try { 156 | InetAddress address = InetAddress.getByName(name); 157 | Allow allow = new Allow(); 158 | allow.setIp(address.getHostAddress()); 159 | allows.add(allow); 160 | } catch (UnknownHostException e) { 161 | LOGGER.error(String.format("[Network/TcpInterface]: Server %s unknown in allowfrom", name)); 162 | } 163 | } 164 | 165 | public void newUser(@NotNull SelectionKey key) { 166 | String peer; 167 | InetSocketAddress remoteAddress; 168 | int ok = 0; 169 | SocketChannel clientChannel; 170 | try { 171 | ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); 172 | clientChannel = serverChannel.accept(); 173 | if (clientChannel == null) { 174 | return; 175 | } 176 | clientChannel.configureBlocking(false); 177 | remoteAddress = (InetSocketAddress) clientChannel.getRemoteAddress(); 178 | for (Allow allow : allows) { 179 | if (allow.getIp().equals(remoteAddress.getAddress().getHostAddress())) { 180 | ok = 1; 181 | break; 182 | } 183 | } 184 | peer = Support.findHostname(remoteAddress.getAddress().getHostAddress()); 185 | if (ok == 0 && !allows.isEmpty()) { 186 | String message = "#You are not allowed on this port.\r\n"; 187 | clientChannel.write(ByteBuffer.wrap(message.getBytes())); 188 | LOGGER.info(String.format("[Network/TcpInterface]: Connection rejected from %s", peer)); 189 | clientChannel.close(); 190 | return; 191 | } 192 | } catch (IOException e) { 193 | LOGGER.error("[Network/TcpInterface]: Accept connection failed.", e); 194 | return; 195 | } 196 | 197 | LOGGER.info(String.format("[Network/TcpInterface]: Connection accepted from %s on %s", peer, description)); 198 | 199 | try { 200 | clientChannel.setOption(TCP_NODELAY, true); 201 | } catch (IOException e) { 202 | LOGGER.error("[Network/TcpInterface]: Could not set off TCP_NODELAY"); 203 | } catch (UnsupportedOperationException e) { 204 | LOGGER.warn("[Network/TcpInterface]: TCP_NODELAY is not supported on current platform"); 205 | } 206 | 207 | newUser(clientChannel, peer, remoteAddress.getPort(), 0); 208 | } 209 | 210 | public void newUser(SocketChannel client, String peerName, int port, int g) { 211 | 212 | } 213 | 214 | public void insertUser(@NotNull AbstractUser user) { 215 | try { 216 | user.getSocketChannel().register(selector, SelectionKey.OP_READ, user); 217 | } catch (ClosedChannelException e) { 218 | e.printStackTrace(); 219 | } 220 | users.add(user); 221 | if (prompt != null) { 222 | user.setPrompt(prompt); 223 | user.printPrompt(); 224 | } 225 | Manage.manager.incVar(varTotal); 226 | Manage.manager.incVar(varCurrent); 227 | int peak = Objects.requireNonNull(Manage.manager.getVar(varPeak)).getValue().getNumber(); 228 | int now = Objects.requireNonNull(Manage.manager.getVar(varCurrent)).getValue().getNumber(); 229 | if (now > peak) { 230 | Manage.manager.setVar(varPeak, now); 231 | } 232 | } 233 | 234 | @Override 235 | public boolean run() { 236 | int busy = 0; 237 | long now = Support.mtime(); 238 | long timeOut = 1000L; 239 | try { 240 | selector.select(timeOut); 241 | Set selectionKeys = selector.selectedKeys(); 242 | for (Iterator iterator = selectionKeys.iterator(); iterator.hasNext(); ) { 243 | SelectionKey key = iterator.next(); 244 | iterator.remove(); 245 | if (!key.isValid()) { 246 | continue; 247 | } 248 | if (key.isAcceptable()) { 249 | newUser(key); 250 | } 251 | 252 | Object att = key.attachment(); 253 | if (att == null) { 254 | continue; 255 | } 256 | AbstractUser temp = (AbstractUser) att; 257 | if (temp.getKillFlag() != 0) { 258 | users.remove(temp); 259 | temp.close(); 260 | if (varCurrent != -1) { 261 | Manage.manager.decVar(varCurrent); 262 | } 263 | } else { 264 | if (key.isWritable()) { 265 | temp.output(); 266 | } 267 | if (key.isReadable()) { 268 | temp.input(); 269 | } 270 | if (temp.run() > 0) { 271 | busy = 1; 272 | } 273 | } 274 | } 275 | } catch (IOException e) { 276 | LOGGER.error("[Network/TcpInterface]: run select failed", e); 277 | } 278 | 279 | for (Iterator iterator = users.iterator(); iterator.hasNext(); ) { 280 | AbstractUser temp = iterator.next(); 281 | if (temp.getKillFlag() != 0) { 282 | iterator.remove(); 283 | temp.close(); 284 | if (varCurrent != -1) { 285 | Manage.manager.decVar(varCurrent); 286 | } 287 | } 288 | } 289 | 290 | if (prevChecks != now) { 291 | doChecks(); 292 | } 293 | return busy > 0; 294 | } 295 | 296 | private void doChecks() { 297 | long now = Support.mtime(); 298 | prevChecks = now; 299 | for (AbstractUser temp : users) { 300 | if (temp.getKillFlag() == 0) { 301 | if (temp.getTimeOut() != 0) { 302 | temp.kill(NetworkConstants.KILL_DATA_TIMEOUT); 303 | } 304 | if ((feedStrategy & ((now - temp.getPrevFeedCheck() > GlobalConstants.USER_FEED_CHECK) ? 1 : 0)) != 0) { 305 | temp.calcFeed(); 306 | } 307 | if (now - temp.getLastPing() >= GlobalConstants.USER_PING_TIMEOUT) { 308 | temp.sendPing(); 309 | temp.setLastPing(now); 310 | } else if (now - temp.getLastActive() >= GlobalConstants.USER_TIMEOUT) { 311 | temp.uprintf("# Timeout\r\n"); 312 | temp.setTimeOut(1); 313 | } 314 | } 315 | } 316 | for (Iterator iterator = guards.iterator(); iterator.hasNext(); ) { 317 | Guard guard = iterator.next(); 318 | long time = Support.mtime(); 319 | if (time - guard.getPrevTry() > GlobalConstants.GUARD_RETRY) { 320 | guard.setPrevTry(time); 321 | if (addUser(guard.getHost(), guard.getPort(), null) != 0) { 322 | iterator.remove(); 323 | } 324 | } 325 | } 326 | } 327 | 328 | public int addUser(String name, int port, @Nullable AbstractUser terminal) { 329 | SocketChannel sock; 330 | for (AbstractUser user : users) { 331 | if (user.getPort() == port && user.getPeer().equals(name)) { 332 | if (terminal != null) { 333 | terminal.uprintf("Already connected to %s\r\n", name); 334 | } 335 | return -1; 336 | } 337 | } 338 | 339 | InetAddress address; 340 | try { 341 | address = InetAddress.getByName(name); 342 | } catch (UnknownHostException e) { 343 | if (terminal != null) { 344 | terminal.uprintf("Unknown hostname: %s\r\n", name); 345 | } 346 | return 0; 347 | } 348 | 349 | try { 350 | sock = SocketChannel.open(); 351 | } catch (IOException e) { 352 | LOGGER.error("[Network/TcpInterface]: Could not open socket channel", e); 353 | return 0; 354 | } 355 | 356 | if (terminal != null) { 357 | terminal.uprintf("[Network/TcpInterface]: Connecting to %s port %d.\r\n", name, port); 358 | } 359 | 360 | int count = 0; 361 | Exception error = null; 362 | while (true) { 363 | if (++count == 3) { 364 | break; 365 | } 366 | try { 367 | sock.connect(new InetSocketAddress(address, port)); 368 | sock.configureBlocking(false); 369 | } catch (ClosedByInterruptException e) { 370 | if (count < 3) { 371 | continue; 372 | } 373 | error = e; 374 | break; 375 | } catch (Exception e) { 376 | error = e; 377 | break; 378 | } 379 | } 380 | if (error != null) { 381 | if (terminal != null) { 382 | terminal.uprintf("Could not connect to server: %s\r\n", error.getMessage()); 383 | } 384 | try { 385 | sock.close(); 386 | } catch (IOException e) { 387 | LOGGER.error("[Network/TcpInterface]: Close socket error.", e); 388 | } 389 | } 390 | if (terminal != null) { 391 | terminal.uprintf("Connection established.\r\n"); 392 | } 393 | try { 394 | sock.setOption(TCP_NODELAY, true); 395 | } catch (IOException e) { 396 | LOGGER.error("[Network/TcpInterface]: Could not set off TCP_NODELAY"); 397 | } catch (UnsupportedOperationException e) { 398 | LOGGER.warn("[Network/TcpInterface]: TCP_NODELAY is not supported on current platform"); 399 | } 400 | newUser(sock, name, port, 1); 401 | return 1; 402 | } 403 | 404 | public void delGuard() { 405 | guards.clear(); 406 | } 407 | 408 | public String getDescription() { 409 | return description; 410 | } 411 | 412 | public void setDescription(String description) { 413 | this.description = description; 414 | } 415 | 416 | public ServerSocketChannel getSock() { 417 | return sock; 418 | } 419 | 420 | public void setSock(ServerSocketChannel sock) { 421 | this.sock = sock; 422 | } 423 | 424 | public int getVarCurrent() { 425 | return varCurrent; 426 | } 427 | 428 | public void setVarCurrent(int varCurrent) { 429 | this.varCurrent = varCurrent; 430 | } 431 | 432 | public int getVarTotal() { 433 | return varTotal; 434 | } 435 | 436 | public void setVarTotal(int varTotal) { 437 | this.varTotal = varTotal; 438 | } 439 | 440 | public int getVarPeak() { 441 | return varPeak; 442 | } 443 | 444 | public void setVarPeak(int varPeak) { 445 | this.varPeak = varPeak; 446 | } 447 | 448 | public int[] getVarClosed() { 449 | return varClosed; 450 | } 451 | 452 | public void setVarClosed(int[] varClosed) { 453 | this.varClosed = varClosed; 454 | } 455 | 456 | public int getFeedStrategy() { 457 | return feedStrategy; 458 | } 459 | 460 | public void setFeedStrategy(int feedStrategy) { 461 | this.feedStrategy = feedStrategy; 462 | } 463 | 464 | public int getFloodLimit() { 465 | return floodLimit; 466 | } 467 | 468 | public void setFloodLimit(int floodLimit) { 469 | this.floodLimit = floodLimit; 470 | } 471 | 472 | public int getOutBufLimit() { 473 | return outBufLimit; 474 | } 475 | 476 | public void setOutBufLimit(int outBufLimit) { 477 | this.outBufLimit = outBufLimit; 478 | } 479 | 480 | public long getPrevChecks() { 481 | return prevChecks; 482 | } 483 | 484 | public void setPrevChecks(long prevChecks) { 485 | this.prevChecks = prevChecks; 486 | } 487 | 488 | public @Nullable String getPrompt() { 489 | return prompt; 490 | } 491 | 492 | public void setPrompt(@Nullable String prompt) { 493 | this.prompt = prompt; 494 | } 495 | 496 | public List getUsers() { 497 | return users; 498 | } 499 | 500 | public void setUsers(List users) { 501 | this.users = users; 502 | } 503 | 504 | public List getGuards() { 505 | return guards; 506 | } 507 | 508 | public void setGuards(List guards) { 509 | this.guards = guards; 510 | } 511 | 512 | public List getAllows() { 513 | return allows; 514 | } 515 | 516 | public void setAllows(List allows) { 517 | this.allows = allows; 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/network/ServerInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 LinkTechTips 3 | */ 4 | 5 | package org.linktechtips.process.network; 6 | 7 | import org.linktechtips.Main; 8 | import org.linktechtips.manager.Manage; 9 | import org.linktechtips.model.Certificate; 10 | import org.linktechtips.model.Client; 11 | import org.linktechtips.model.Flightplan; 12 | import org.linktechtips.model.Server; 13 | import org.linktechtips.support.Support; 14 | import org.linktechtips.user.AbstractUser; 15 | import org.linktechtips.user.ServerUser; 16 | import org.linktechtips.weather.WProfile; 17 | import org.linktechtips.constants.*; 18 | import org.jetbrains.annotations.NotNull; 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.nio.channels.SocketChannel; 22 | 23 | public class ServerInterface extends TcpInterface { 24 | private int packetCount, varMcDrops, varIntErr; 25 | 26 | private int varMcHandled, varUcHandled, varUcOverRun, varFailed, varShape, varBounce; 27 | 28 | private String serverIdent; 29 | 30 | private long lastSync; 31 | 32 | public ServerInterface(int port, String code, String d) { 33 | super(port, code, d); 34 | packetCount = 0; 35 | varMcHandled = Manage.manager.addVar("protocol.multicast.handled", ManageVarType.ATT_INT); 36 | varMcDrops = Manage.manager.addVar("protocol.multicast.dropped", ManageVarType.ATT_INT); 37 | varUcHandled = Manage.manager.addVar("protocol.unicast.handled", ManageVarType.ATT_INT); 38 | varUcOverRun = Manage.manager.addVar("protocol.unicast.overruns", ManageVarType.ATT_INT); 39 | varFailed = Manage.manager.addVar("protocol.errors.invalidcommand", ManageVarType.ATT_INT); 40 | varBounce = Manage.manager.addVar("protocol.errors.bounce", ManageVarType.ATT_INT); 41 | varShape = Manage.manager.addVar("protocol.errors.shape", ManageVarType.ATT_INT); 42 | varIntErr = Manage.manager.addVar("protocol.errors.integer", ManageVarType.ATT_INT); 43 | serverIdent = Server.myServer.getIdent(); 44 | } 45 | 46 | @Override 47 | public boolean run() { 48 | long now = Support.mtime(); 49 | boolean busy = super.run(); 50 | if (now - lastSync > GlobalConstants.SYNC_TIMEOUT) { 51 | sendSync(); 52 | } 53 | for (AbstractUser temp : users) { 54 | ServerUser stemp = (ServerUser) temp; 55 | if (stemp.getClientOk() == 0 && now - stemp.getStartupTime() > GlobalConstants.SERVER_MAX_TOOK) { 56 | stemp.kill(NetworkConstants.KILL_INIT_TIMEOUT); 57 | } 58 | } 59 | return busy; 60 | } 61 | 62 | @Override 63 | public void newUser(SocketChannel client, String peerName, int port, int g) { 64 | insertUser(new ServerUser(client, this, peerName, port, g)); 65 | } 66 | 67 | public void incPacketCount() { 68 | packetCount++; 69 | /* Now check if the integer looped */ 70 | if (packetCount < 0 || packetCount > 2_000_000_000) { 71 | sendReset(); 72 | } 73 | } 74 | 75 | /* Send a SYNC packet */ 76 | public void sendSync() { 77 | sendPacket(null, null, ProtocolConstants.CMD_SYNC, "*", serverIdent, packetCount, 0, 0, ""); 78 | lastSync = Support.mtime(); 79 | incPacketCount(); 80 | } 81 | 82 | /* Send a NOTIFY packet to a destination */ 83 | public void sendServerNotify(@NotNull String dest, @NotNull Server subject, AbstractUser toWho) { 84 | String buf = String.format("%d:%s:%s:%s:%s:%s:%d:%s", subject == Server.myServer ? 0 : 1, subject.getIdent(), 85 | subject.getName(), subject.getEmail(), subject.getHostName(), subject.getVersion(), 86 | subject.getFlags(), subject.getLocation()); 87 | sendPacket(null, toWho, ProtocolConstants.CMD_NOTIFY, dest, serverIdent, packetCount, 0, 1, buf); 88 | incPacketCount(); 89 | } 90 | 91 | /* Send a REQMETAR packet */ 92 | public void sendReqMetar(String client, String metar, int fd, int parsed, @NotNull Server dest) { 93 | String buf = String.format("%s:%s:%d:%d", client, metar, parsed, fd); 94 | sendPacket(null, null, ProtocolConstants.CMD_REQ_METAR, dest.getIdent(), serverIdent, 95 | packetCount, 0, 0, buf); 96 | incPacketCount(); 97 | } 98 | 99 | /* Send a LINKDOWN packet to a destination */ 100 | public void sendLinkDown(String data) { 101 | sendPacket(null, null, ProtocolConstants.CMD_LINK_DOWN, "*", serverIdent, packetCount, 102 | 0, 1, data); 103 | incPacketCount(); 104 | } 105 | 106 | /* Send a PONG packet to a destination */ 107 | public void sendPong(@NotNull String dest, String data) { 108 | sendPacket(null, null, ProtocolConstants.CMD_PONG, dest, serverIdent, packetCount, 0, 0, data); 109 | incPacketCount(); 110 | } 111 | 112 | /* Broadcast a PING packet to a destination*/ 113 | public void sendPing(@NotNull String dest, String data) { 114 | sendPacket(null, null, ProtocolConstants.CMD_PING, dest, serverIdent, packetCount, 0, 0, data); 115 | incPacketCount(); 116 | } 117 | 118 | /* Send an ADDCLIENT packet to a destination */ 119 | public void sendAddClient(@NotNull String dest, @NotNull Client who, AbstractUser direction, AbstractUser source, int feed) { 120 | String buf = String.format("%s:%s:%s:%d:%d:%s:%s:%d", who.getCid(), who.getLocation().getIdent(), 121 | who.getCallsign(), who.getType(), who.getRating(), who.getProtocol(), who.getRealName(), who.getSimType()); 122 | sendPacket(null, direction, ProtocolConstants.CMD_ADD_CLIENT, dest, serverIdent, packetCount, 0, 0, buf); 123 | incPacketCount(); 124 | /* This command is important for clients as well, so if it's not a feed, 125 | clients should get it as well */ 126 | if (feed == 0) { 127 | if (who.getType() == ClientConstants.CLIENT_ATC) { 128 | Main.clientInterface.sendAa(who, source); 129 | } else if (who.getType() == ClientConstants.CLIENT_PILOT) { 130 | Main.clientInterface.sendAp(who, source); 131 | } 132 | } 133 | } 134 | 135 | /* Send an RMCLIENT packet to a destination */ 136 | public void sendRmClient(AbstractUser direction, @NotNull String dest, @NotNull Client who, AbstractUser ex) { 137 | String buf = who.getCallsign(); 138 | sendPacket(null, direction, ProtocolConstants.CMD_RM_CLIENT, dest, serverIdent, packetCount, 0, 0, buf); 139 | incPacketCount(); 140 | /* This command is important for clients as well, so if it's send to the 141 | broadcast address, clients should get it as well */ 142 | if (dest.equals("*")) { 143 | if (who.getType() == ClientConstants.CLIENT_ATC) { 144 | Main.clientInterface.sendDa(who, ex); 145 | } else { 146 | Main.clientInterface.sendDp(who, ex); 147 | } 148 | } 149 | } 150 | 151 | public void sendPilotData(@NotNull Client who, AbstractUser ex) { 152 | String data = String.format("%s:%s:%d:%d:%.5f:%.5f:%d:%d:%d:%d", who.getIdentFlag(), 153 | who.getCallsign(), who.getTransponder(), who.getRating(), who.getLat(), who.getLon(), 154 | who.getAltitude(), who.getGroundSpeed(), who.getPbh(), who.getFlags()); 155 | sendPacket(null, null, ProtocolConstants.CMD_PD, "*", serverIdent, packetCount, 0, 0, data); 156 | Main.clientInterface.sendPilotPos(who, ex); 157 | incPacketCount(); 158 | } 159 | 160 | public void sendAtcData(@NotNull Client who, AbstractUser ex) { 161 | String data = String.format("%s:%d:%d:%d:%d:%.5f:%.5f:%d", who.getCallsign(), 162 | who.getFrequency(), who.getFacilityType(), who.getVisualRange(), who.getRating(), who.getLat(), who.getLon(), 163 | who.getAltitude()); 164 | sendPacket(null, null, ProtocolConstants.CMD_AD, "*", serverIdent, packetCount, 0, 0, data); 165 | Main.clientInterface.sendAtcPos(who, ex); 166 | incPacketCount(); 167 | } 168 | 169 | public void sendCert(@NotNull String dest, int cmd, @NotNull Certificate who, AbstractUser direction) { 170 | String data = String.format("%d:%s:%s:%d:%d:%s", cmd, who.getCid(), who.getPassword(), who.getLevel(), 171 | who.getCreation(), who.getOrigin()); 172 | sendPacket(null, direction, ProtocolConstants.CMD_CERT, dest, serverIdent, packetCount, 0, 0, data); 173 | incPacketCount(); 174 | } 175 | 176 | public void sendReset() { 177 | sendPacket(null, null, ProtocolConstants.CMD_RESET, "*", serverIdent, packetCount, 0, 0, ""); 178 | packetCount = 0; 179 | } 180 | 181 | public int sendMulticast(@Nullable Client source, @NotNull String dest, String s, int cmd, int multiOk, AbstractUser ex) { 182 | Client destination = null; 183 | String servDest; 184 | String sourceStr = source != null ? source.getCallsign() : "server"; 185 | if (source != null && dest.equalsIgnoreCase("server")) { 186 | if (cmd == ProtocolConstants.CL_PING) { 187 | Main.clientInterface.sendGeneric(source.getCallsign(), source, null, 188 | null, "server", s, ProtocolConstants.CL_PONG); 189 | } 190 | return 1; 191 | } 192 | switch (dest.charAt(0)) { 193 | case '@', '*' -> { 194 | if (multiOk == 0) { 195 | return 0; 196 | } 197 | servDest = dest; 198 | } 199 | default -> { 200 | servDest = String.format("%%%s", dest); 201 | destination = Client.getClient(dest); 202 | if (destination == null) { 203 | return 0; 204 | } 205 | } 206 | } 207 | String data = String.format("%d:%s:%s", cmd, sourceStr, s); 208 | sendPacket(null, null, ProtocolConstants.CMD_MULTICAST, servDest, serverIdent, packetCount, 0, 0, data); 209 | incPacketCount(); 210 | Main.clientInterface.sendGeneric(dest, destination, ex, source, sourceStr, s, cmd); 211 | return 1; 212 | } 213 | 214 | public void sendPlan(@NotNull String dest, @NotNull Client who, AbstractUser direction) { 215 | Flightplan plan = who.getPlan(); 216 | if (plan == null) { 217 | return; 218 | } 219 | String buf = String.format("%s:%d:%c:%s:%d:%s:%d:%d:%s:%s:%d:%d:%d:%d:%s:%s:%s", 220 | who.getCallsign(), plan.getRevision(), plan.getType(), plan.getAircraft(), 221 | plan.getTasCruise(), plan.getDepAirport(), plan.getDepTime(), 222 | plan.getActDepTime(), plan.getAlt(), plan.getDestAirport(), plan.getHrsEnroute(), 223 | plan.getMinEnroute(), plan.getHrsFuel(), plan.getMinFuel(), plan.getAltAirport(), 224 | plan.getRemarks(), plan.getRoute()); 225 | sendPacket(null, direction, ProtocolConstants.CMD_PLAN, dest, serverIdent, packetCount, 0, 0, buf); 226 | incPacketCount(); 227 | Main.clientInterface.sendPlan(null, who, 400); 228 | } 229 | 230 | public void sendWeather(@NotNull String dest, int fd, @NotNull WProfile w) { 231 | if (dest.equalsIgnoreCase(serverIdent)) { 232 | Main.systemInterface.receiveWeather(fd, w); 233 | return; 234 | } 235 | 236 | if (dest.charAt(0) == '%') { 237 | Client c = Client.getClient(dest.substring(1)); 238 | if (c == null) { 239 | return; 240 | } 241 | if (c.getLocation() == Server.myServer) { 242 | Main.clientInterface.sendWeather(c, w); 243 | return; 244 | } 245 | } 246 | 247 | String weather = w.print(); 248 | String data = String.format("%s:%d:%s", w.getName(), fd, weather); 249 | sendPacket(null, null, ProtocolConstants.CMD_WEATHER, dest, serverIdent, packetCount, 0, 0, data); 250 | incPacketCount(); 251 | } 252 | 253 | public void sendMetar(@NotNull String dest, int fd, String station, String data) { 254 | if (dest.equalsIgnoreCase(serverIdent)) { 255 | Main.systemInterface.receiveMetar(fd, station, data); 256 | return; 257 | } 258 | if (dest.charAt(0) == '%') { 259 | Client c = Client.getClient(dest.substring(1)); 260 | if (c == null) { 261 | return; 262 | } 263 | if (c.getLocation() == Server.myServer) { 264 | Main.clientInterface.sendMetar(c, data); 265 | return; 266 | } 267 | } 268 | String buf = String.format("%s:%d:%s", station, fd, data); 269 | sendPacket(null, null, ProtocolConstants.CMD_METAR, dest, serverIdent, packetCount, 0, 0, buf); 270 | incPacketCount(); 271 | } 272 | 273 | public void sendNoWx(@NotNull String dest, int fd, String station) { 274 | if (dest.equalsIgnoreCase(serverIdent)) { 275 | Main.systemInterface.receiveNoWx(fd, station); 276 | return; 277 | } 278 | if (dest.charAt(0) == '%') { 279 | Client c = Client.getClient(dest.substring(1)); 280 | if (c == null) { 281 | return; 282 | } 283 | if (c.getLocation() == Server.myServer) { 284 | Main.clientInterface.sendNoWx(c, station); 285 | return; 286 | } 287 | } 288 | String buf = String.format("%s:%d", station, fd); 289 | sendPacket(null, null, ProtocolConstants.CMD_NO_WX, dest, serverIdent, packetCount, 0, 0, buf); 290 | incPacketCount(); 291 | } 292 | 293 | public void sendAddWp(AbstractUser direction, @NotNull WProfile wp) { 294 | String weather = wp.print(); 295 | String buf = String.format("%s:%d:%s:%s", wp.getName(), wp.getVersion(), wp.getOrigin(), weather); 296 | sendPacket(null, direction, ProtocolConstants.CMD_ADD_WPROFILE, "*", serverIdent, packetCount, 0, 0, buf); 297 | incPacketCount(); 298 | } 299 | 300 | public void sendDelWp(@NotNull WProfile wp) { 301 | sendPacket(null, null, ProtocolConstants.CMD_DEL_WPROFILE, "*", serverIdent, packetCount, 0, 0, wp.getName()); 302 | incPacketCount(); 303 | } 304 | 305 | public void sendKill(@NotNull Client who, String reason) { 306 | if (who.getLocation() == Server.myServer) { 307 | Main.clientInterface.handleKill(who, reason); 308 | return; 309 | } 310 | String data = String.format("%s:%s", who.getCallsign(), reason); 311 | sendPacket(null, null, ProtocolConstants.CMD_KILL, who.getLocation().getIdent(), serverIdent, 312 | packetCount, 0, 1, data); 313 | incPacketCount(); 314 | } 315 | 316 | /* Send the packet on its way. This function will also do the routing */ 317 | public void sendPacket(AbstractUser exclude, @Nullable AbstractUser direction, int cmdNum, 318 | @NotNull String to, String from, int pc, int hc, int bi, String data) { 319 | StringBuilder buf = new StringBuilder(); 320 | /* variable to determine wheter or not to do the softlimit check on the 321 | server connection output buffer. currently only do the check on 322 | client position updates */ 323 | int slCheck = (cmdNum == ProtocolConstants.CMD_PD || cmdNum == ProtocolConstants.CMD_AD) ? 1 : 0; 324 | 325 | /* Increase the hopcount */ 326 | hc++; 327 | 328 | /* Assemble the packet */ 329 | assemble(buf, cmdNum, to, from, bi, pc, hc, data); 330 | 331 | 332 | //TODO 333 | // if (direction != null) { 334 | // direction.uslprintf("%s\n", slCheck); 335 | // return; 336 | //} 337 | 338 | /* Now look at the destionation field, to determine the route for this 339 | packet */ 340 | 341 | switch (to.charAt(0)) { 342 | case '@': /* Fallthrough to broadcast */ 343 | 344 | /* This is a broadcast packet. 345 | Note: '*P' and '*A' are broadcast destinations too! */ 346 | case '*': 347 | /* Reassemble the packet to indicate a broadcast */ 348 | if (bi == 0) { 349 | assemble(buf, cmdNum, to, from, 1, pc, hc, data); 350 | } 351 | for (AbstractUser tempUser : users) { 352 | if (isServerOk((ServerUser) tempUser, exclude, cmdNum)) { 353 | tempUser.uslprintf("%s\r\n", slCheck, buf); 354 | } 355 | } 356 | break; 357 | /* This is a packet for a pilot. Lookup the pilot, and send in the 358 | direction of the appropriate server */ 359 | case '%': 360 | for (Client tempClient : Client.clients) { 361 | if (tempClient.getCallsign().equals(to.substring(1))) { 362 | /* We got the pilot, and his connected server, now if we know the 363 | correct path, send it; otherwise we'll have to broadcast the 364 | packet */ 365 | Server tempServer = tempClient.getLocation(); 366 | /* It the pilot is connected to this server, don't send out 367 | the packet to other servers */ 368 | if (tempServer == Server.myServer) { 369 | break; 370 | } 371 | if (tempServer.getPath() != null) { 372 | tempServer.getPath().uslprintf("%s\r\n", slCheck, buf); 373 | } else { 374 | /* Reassemble the packet to indicate a broadcast */ 375 | if (bi == 0) { 376 | assemble(buf, cmdNum, to, from, 1, pc, hc, data); 377 | } 378 | for (AbstractUser tempUser : users) { 379 | if (isServerOk((ServerUser) tempUser, exclude, cmdNum)) { 380 | tempUser.uslprintf("%s\r\n", slCheck, buf); 381 | } 382 | } 383 | } 384 | break; 385 | } 386 | } 387 | break; 388 | 389 | /* This packet is on its way to a single server */ 390 | default: 391 | for (Server tempServer : Server.servers) { 392 | if (tempServer.getIdent().equals(to)) { 393 | if (tempServer.getPath() != null) { 394 | tempServer.getPath().uslprintf("%s\r\n", slCheck, buf); 395 | } else { 396 | /* Reassemble the packet to indicate a broadcast */ 397 | if (bi == 0) { 398 | assemble(buf, cmdNum, to, from, 1, pc, hc, data); 399 | } 400 | for (AbstractUser tempUser : users) { 401 | if (isServerOk((ServerUser) tempUser, exclude, cmdNum)) { 402 | tempUser.uslprintf("%s\r\n", slCheck, buf); 403 | } 404 | } 405 | } 406 | } 407 | break; 408 | } 409 | break; 410 | } 411 | } 412 | 413 | /* This routine is called whenever a client is dropped. We have to check 414 | here if there's a silent server connected to us. In that case, we'll 415 | have to send him a RMCLIENT */ 416 | public void clientDropped(@NotNull Client who) { 417 | for (AbstractUser temp : users) { 418 | ServerUser s = (ServerUser) temp; 419 | if (s.getThisServer() != null && (s.getThisServer().getFlags() & ServerConstants.SERVER_SILENT) != 0) { 420 | sendRmClient(s, s.getThisServer().getName(), who, null); 421 | } 422 | } 423 | } 424 | 425 | private boolean isServerOk(@NotNull ServerUser x, AbstractUser exclude, int cmdNum) { 426 | return (x != exclude) && (ProtocolConstants.SILENT_OK[cmdNum] == 1 || x.getThisServer() == null || 427 | (x.getThisServer().getFlags() & ServerConstants.SERVER_SILENT) == 0); 428 | } 429 | 430 | public void assemble(@NotNull StringBuilder buf, int cmdNum, String to, String from, int bi, int pc, int hc, String data) { 431 | buf.append(String.format("%s:%s:%s:%c%d:%d:%s", ProtocolConstants.CMD_NAMES[cmdNum], to, from, 432 | bi > 0 ? 'B' : 'U', pc, hc, data)); 433 | } 434 | 435 | public int getPacketCount() { 436 | return packetCount; 437 | } 438 | 439 | public void setPacketCount(int packetCount) { 440 | this.packetCount = packetCount; 441 | } 442 | 443 | public int getVarMcDrops() { 444 | return varMcDrops; 445 | } 446 | 447 | public void setVarMcDrops(int varMcDrops) { 448 | this.varMcDrops = varMcDrops; 449 | } 450 | 451 | public int getVarIntErr() { 452 | return varIntErr; 453 | } 454 | 455 | public void setVarIntErr(int varIntErr) { 456 | this.varIntErr = varIntErr; 457 | } 458 | 459 | public int getVarMcHandled() { 460 | return varMcHandled; 461 | } 462 | 463 | public void setVarMcHandled(int varMcHandled) { 464 | this.varMcHandled = varMcHandled; 465 | } 466 | 467 | public int getVarUcHandled() { 468 | return varUcHandled; 469 | } 470 | 471 | public void setVarUcHandled(int varUcHandled) { 472 | this.varUcHandled = varUcHandled; 473 | } 474 | 475 | public int getVarUcOverRun() { 476 | return varUcOverRun; 477 | } 478 | 479 | public void setVarUcOverRun(int varUcOverRun) { 480 | this.varUcOverRun = varUcOverRun; 481 | } 482 | 483 | public int getVarFailed() { 484 | return varFailed; 485 | } 486 | 487 | public void setVarFailed(int varFailed) { 488 | this.varFailed = varFailed; 489 | } 490 | 491 | public int getVarShape() { 492 | return varShape; 493 | } 494 | 495 | public void setVarShape(int varShape) { 496 | this.varShape = varShape; 497 | } 498 | 499 | public int getVarBounce() { 500 | return varBounce; 501 | } 502 | 503 | public void setVarBounce(int varBounce) { 504 | this.varBounce = varBounce; 505 | } 506 | 507 | public String getServerIdent() { 508 | return serverIdent; 509 | } 510 | 511 | public void setServerIdent(String serverIdent) { 512 | this.serverIdent = serverIdent; 513 | } 514 | 515 | public long getLastSync() { 516 | return lastSync; 517 | } 518 | 519 | public void setLastSync(long lastSync) { 520 | this.lastSync = lastSync; 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /src/main/java/org/linktechtips/process/metar/MetarManage.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2022 LinkTechTips 4 | */ 5 | 6 | package org.linktechtips.process.metar; 7 | 8 | import org.linktechtips.Main; 9 | import org.linktechtips.constants.FsdPath; 10 | import org.linktechtips.constants.ManageVarType; 11 | import org.linktechtips.constants.MetarSource; 12 | import org.linktechtips.constants.ServerConstants; 13 | import org.linktechtips.manager.Manage; 14 | import org.linktechtips.model.Server; 15 | import org.linktechtips.process.Process; 16 | import org.linktechtips.process.config.ConfigEntry; 17 | import org.linktechtips.process.config.ConfigGroup; 18 | import org.linktechtips.support.Support; 19 | import org.linktechtips.weather.WProfile; 20 | import org.linktechtips.weather.Weather; 21 | import org.apache.commons.lang3.StringUtils; 22 | import org.apache.commons.lang3.math.NumberUtils; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.io.*; 29 | import java.net.InetAddress; 30 | import java.net.Socket; 31 | import java.net.UnknownHostException; 32 | import java.nio.file.Files; 33 | import java.nio.file.Path; 34 | import java.nio.file.Paths; 35 | import java.nio.file.attribute.BasicFileAttributes; 36 | import java.util.*; 37 | 38 | public class MetarManage extends Process { 39 | 40 | private final static Logger LOGGER = LoggerFactory.getLogger(MetarManage.class); 41 | 42 | public static MetarManage metarManager; 43 | 44 | public static final int VAR_AMOUNT = 10; 45 | public static final int MAX_METAR_DOWNLOAD_TIME = 1600_000; 46 | 47 | private @Nullable BufferedWriter ioIn; 48 | 49 | private @Nullable BufferedReader ioOut; 50 | 51 | private @Nullable Socket sock; 52 | 53 | private @Nullable BufferedReader dataReadSock; 54 | 55 | private @Nullable BufferedWriter dataSock; 56 | 57 | private @Nullable Mmq rootQ; 58 | 59 | private long prevDownload; 60 | 61 | private long metarFileTime; 62 | 63 | private int nStations; 64 | 65 | private final @NotNull List stationList; 66 | 67 | private int metarSize; 68 | 69 | private boolean newFileReady; 70 | 71 | private final int varPrev; 72 | 73 | private final int varTotal; 74 | 75 | private final int varStations; 76 | 77 | private final int @NotNull [] variation; 78 | 79 | private String metarHost; 80 | 81 | private String metarDir; 82 | 83 | private String ftpEmail; 84 | 85 | private boolean passiveMode; 86 | 87 | private boolean downloading; 88 | 89 | private int source; 90 | 91 | public MetarManage() { 92 | passiveMode = true; 93 | source = MetarSource.SOURCE_NETWORK; 94 | rootQ = null; 95 | int prevHour = 1; 96 | nStations = 0; 97 | stationList = new ArrayList<>(); 98 | variation = new int[VAR_AMOUNT]; 99 | 100 | parseWeatherConfig(); 101 | parseSystemConfig(); 102 | 103 | if (source == MetarSource.SOURCE_DOWNLOAD) { 104 | if (metarHost == null) { 105 | metarHost = "weather.noaa.gov"; 106 | } 107 | 108 | if (metarDir == null) { 109 | metarDir = "data/observations/metar/cycles/"; 110 | } 111 | } 112 | 113 | int var = Manage.manager.addVar("metar.method", ManageVarType.ATT_VARCHAR); 114 | if (source == MetarSource.SOURCE_NETWORK) { 115 | Manage.manager.setVar(var, "network"); 116 | } else if (source == MetarSource.SOURCE_FILE) { 117 | Manage.manager.setVar(var, "file"); 118 | } else if (source == MetarSource.SOURCE_DOWNLOAD) { 119 | Manage.manager.setVar(var, "download"); 120 | } 121 | 122 | varPrev = Manage.manager.addVar("metar.current", ManageVarType.ATT_DATE); 123 | varTotal = Manage.manager.addVar("metar.requests", ManageVarType.ATT_INT); 124 | varStations = Manage.manager.addVar("metar.stations", ManageVarType.ATT_INT); 125 | 126 | Manage.manager.setVar(varPrev, Support.mtime()); 127 | 128 | if (source == MetarSource.SOURCE_FILE) { 129 | buildList(); 130 | } 131 | } 132 | 133 | private void parseWeatherConfig() { 134 | ConfigGroup weatherGroup = Main.configManager.getGroup("weather"); 135 | ConfigEntry sourceEntry = null, serverEntry = null, dirEntry = null, ftpModeEntry = null; 136 | if (weatherGroup != null) { 137 | sourceEntry = weatherGroup.getEntry("source"); 138 | serverEntry = weatherGroup.getEntry("server"); 139 | dirEntry = weatherGroup.getEntry("dir"); 140 | ftpModeEntry = weatherGroup.getEntry("ftpmode"); 141 | } 142 | 143 | if (sourceEntry != null) { 144 | String source = sourceEntry.getData(); 145 | if ("network".equals(source)) { 146 | this.source = MetarSource.SOURCE_NETWORK; 147 | } else if ("file".equals(source)) { 148 | this.source = MetarSource.SOURCE_FILE; 149 | } else if ("download".equals(source)) { 150 | this.source = MetarSource.SOURCE_DOWNLOAD; 151 | } else { 152 | LOGGER.error(String.format("[METAR]: Unknown METAR source %s in config file", source)); 153 | } 154 | } 155 | 156 | if (serverEntry != null) { 157 | metarHost = serverEntry.getData(); 158 | } 159 | 160 | if (dirEntry != null) { 161 | metarDir = dirEntry.getData(); 162 | } 163 | 164 | if (ftpModeEntry != null) { 165 | String mode = ftpModeEntry.getData(); 166 | passiveMode = "passive".equals(mode); 167 | } 168 | } 169 | 170 | private void parseSystemConfig() { 171 | ConfigGroup systemGroup = Main.configManager.getGroup("system"); 172 | ConfigEntry emailEntry = null; 173 | if (systemGroup != null) { 174 | emailEntry = systemGroup.getEntry("email"); 175 | } 176 | 177 | if (emailEntry != null) { 178 | ftpEmail = emailEntry.getData(); 179 | } 180 | } 181 | 182 | private void buildList() { 183 | Path file = Paths.get(FsdPath.METARFILE); 184 | if (Files.notExists(file)) { 185 | return; 186 | } 187 | try { 188 | BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class); 189 | metarFileTime = attr.lastModifiedTime().toMillis(); 190 | } catch (IOException e) { 191 | return; 192 | } 193 | 194 | nStations = 0; 195 | stationList.clear(); 196 | parseMetar(); 197 | } 198 | 199 | private void parseMetar() { 200 | File file = new File(FsdPath.METARFILE); 201 | if (!file.isFile() || !file.exists()) { 202 | return; 203 | } 204 | try (InputStreamReader read = new InputStreamReader(new FileInputStream(file)); 205 | BufferedReader bufferedReader = new BufferedReader(read)) { 206 | String line; 207 | List arr = new ArrayList<>(); 208 | for (long offset = 0; (line = bufferedReader.readLine()) != null; offset += line.length()) { 209 | if (line.length() < 30) { 210 | continue; 211 | } 212 | 213 | int count = Support.breakArgs(line, arr, 3); 214 | if (count < 3) { 215 | continue; 216 | } 217 | if (line.startsWith(" ")) { 218 | continue; 219 | } 220 | String stationName = null; 221 | if (arr.get(0).length() == 4) { 222 | stationName = arr.get(0); 223 | } else if (arr.get(1).length() == 4) { 224 | stationName = arr.get(1); 225 | } 226 | if (stationName == null) { 227 | continue; 228 | } 229 | Station station = new Station(); 230 | station.setName(stationName); 231 | station.setLocation(offset); 232 | stationList.add(station); 233 | nStations++; 234 | } 235 | stationList.sort(Comparator.comparing(Station::getName)); 236 | Manage.manager.setVar(varStations, nStations); 237 | } catch (FileNotFoundException e) { 238 | LOGGER.error("[METAR]: Config file not found: " + FsdPath.METARFILE); 239 | } catch (IOException e) { 240 | LOGGER.error("[METAR]: Something went wrong when parse metar file: ", e); 241 | } 242 | } 243 | 244 | private void setupNewFile() { 245 | newFileReady = false; 246 | File metarFile = new File(FsdPath.METARFILE); 247 | File metarNewFile = new File(FsdPath.METARFILENEW); 248 | if (metarSize < 10_0000) { 249 | metarNewFile.delete(); 250 | LOGGER.warn(String.format("[METAR]: Size of new METAR file (%d) is too small, dropping.", metarSize)); 251 | } 252 | 253 | if (!metarNewFile.renameTo(metarFile)) { 254 | LOGGER.warn(String.format("[METAR]: Can't move %s to %s", FsdPath.METARFILENEW, FsdPath.METARFILE)); 255 | } else { 256 | LOGGER.info("[METAR]: Installed new METAR data."); 257 | } 258 | buildList(); 259 | Manage.manager.setVar(varPrev, Support.mtime()); 260 | } 261 | 262 | @Override 263 | public boolean run() { 264 | if (source == MetarSource.SOURCE_NETWORK) { 265 | return true; 266 | } 267 | 268 | if (downloading) { 269 | doDownload(); 270 | } 271 | 272 | while (rootQ != null) { 273 | if (doParse(rootQ) == 0) { 274 | break; 275 | } 276 | } 277 | 278 | if (!downloading && newFileReady) { 279 | setupNewFile(); 280 | } 281 | 282 | return true; 283 | } 284 | 285 | public void startDownload() { 286 | if (downloading) { 287 | LOGGER.error("[METAR]: server seems to be still loading"); 288 | return; 289 | } 290 | 291 | prevDownload = Support.mtime(); 292 | try { 293 | File metarFileNew = new File(FsdPath.METARFILENEW); 294 | if (!metarFileNew.exists()) { 295 | metarFileNew.createNewFile(); 296 | } 297 | ioIn = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metarFileNew))); 298 | } catch (IOException e) { 299 | LOGGER.error("[METAR]: Open metarnew.txt failed."); 300 | return; 301 | } 302 | InetAddress address; 303 | try { 304 | address = InetAddress.getByName(metarHost); 305 | } catch (UnknownHostException e) { 306 | LOGGER.error(String.format("[METAR]: Could not lookup METAR host name %s.", metarHost)); 307 | stopDownload(); 308 | return; 309 | } 310 | 311 | try { 312 | sock = new Socket(address, 21); 313 | dataReadSock = new BufferedReader(new InputStreamReader(sock.getInputStream())); 314 | dataSock = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream())); 315 | } catch (IOException e) { 316 | LOGGER.error("[METAR]: Could not connect to ftp port on METAR host"); 317 | stopDownload(); 318 | return; 319 | } 320 | 321 | try { 322 | if (passiveMode) { 323 | String data = String.format("USER anonymous\nPASS %s\nCWD %s\nPASV\n", ftpEmail, metarDir); 324 | dataSock.write(data); 325 | downloading = true; 326 | metarSize = 0; 327 | dataReadSock.close(); 328 | dataReadSock = null; 329 | } else { 330 | String url = "127.0.0.1"; 331 | int dataPort = (int) (Math.random() * 100000 % 9999) + 1024; 332 | String port = String.format("127,0,0,1,%d", dataPort); 333 | String data = String.format("USER anonymous\nPASS %s\nCWD %s\nPORT %s\nRETR %02dZ.TXT\n", 334 | ftpEmail, metarDir, port, (Calendar.getInstance().get(Calendar.HOUR_OF_DAY) + 21) % 24); 335 | dataSock.write(data); 336 | LOGGER.info("[METAR]: Starting download of METAR data"); 337 | downloading = true; 338 | metarSize = 0; 339 | dataReadSock.close(); 340 | dataReadSock = null; 341 | } 342 | } catch (IOException e) { 343 | LOGGER.error("[METAR]: Socket operation failed."); 344 | stopDownload(); 345 | } 346 | } 347 | 348 | private void doDownload() { 349 | long now = Support.mtime(); 350 | if (now - prevDownload > MAX_METAR_DOWNLOAD_TIME) { 351 | LOGGER.warn("[METAR]: METAR download interrupted due to timeout"); 352 | stopDownload(); 353 | startDownload(); 354 | return; 355 | } 356 | try { 357 | if (Objects.requireNonNull(dataReadSock).ready()) { 358 | char[] buf = new char[4096]; 359 | int bytes = dataReadSock.read(buf); 360 | if (bytes <= 0) { 361 | stopDownload(); 362 | newFileReady = true; 363 | } else if (passiveMode) { 364 | String response = new String(buf); 365 | String passHost; 366 | int passPort; 367 | if (!response.startsWith("2271 ")) { 368 | LOGGER.error(String.format("[METAR]: Could not request passive mode: %s", response)); 369 | stopDownload(); 370 | return; 371 | } 372 | int opening = response.indexOf('('); 373 | int closing = response.indexOf(')', opening + 1); 374 | if (closing > 0) { 375 | String dataLink = response.substring(opening + 1, closing); 376 | StringTokenizer tokenizer = new StringTokenizer(dataLink, ","); 377 | try { 378 | passHost = tokenizer.nextToken() + "." + tokenizer.nextToken() + "." 379 | + tokenizer.nextToken() + "." + tokenizer.nextToken(); 380 | passPort = NumberUtils.toInt(tokenizer.nextToken()) * 256 381 | + NumberUtils.toInt(tokenizer.nextToken()); 382 | } catch (Exception e) { 383 | LOGGER.error( 384 | "[METAR]: Received bad data link information: " 385 | + response); 386 | stopDownload(); 387 | return; 388 | } 389 | Socket dataSocket; 390 | try { 391 | dataSocket = new Socket(passHost, passPort); 392 | dataReadSock = new BufferedReader(new InputStreamReader(dataSocket.getInputStream())); 393 | String data = String.format("RETR %02dZ.TXT\n", (Calendar.getInstance().get(Calendar.HOUR_OF_DAY) + 21) % 24); 394 | Objects.requireNonNull(dataSock).write(data); 395 | LOGGER.info("[METAR]: Starting download of METAR data"); 396 | } catch (IOException e) { 397 | LOGGER.error("[METAR]: Could not connect to ftp port on METAR host"); 398 | stopDownload(); 399 | return; 400 | } 401 | } 402 | } 403 | try { 404 | String data = dataReadSock.readLine(); 405 | metarSize = data.length(); 406 | Files.writeString(Paths.get(FsdPath.METARFILENEW), data); 407 | } catch (IOException e) { 408 | LOGGER.error("[METAR]: Download METAR data error."); 409 | stopDownload(); 410 | return; 411 | } 412 | stopDownload(); 413 | newFileReady = true; 414 | } 415 | } catch (IOException e) { 416 | LOGGER.error("[METAR]: Start to download of METAR data failed."); 417 | stopDownload(); 418 | } 419 | } 420 | 421 | public void stopDownload() { 422 | try { 423 | if (dataReadSock != null) { 424 | dataReadSock.close(); 425 | dataReadSock = null; 426 | } 427 | 428 | if (dataSock != null) { 429 | dataSock.close(); 430 | dataSock = null; 431 | } 432 | 433 | if (sock != null) { 434 | sock.close(); 435 | sock = null; 436 | } 437 | ioIn = null; 438 | downloading = false; 439 | } catch (IOException e) { 440 | LOGGER.error("[METAR]: Close socket failed."); 441 | } 442 | } 443 | 444 | private void checkMetarFile() { 445 | try { 446 | Path file = Paths.get(FsdPath.METARFILE); 447 | BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class); 448 | long modifyTime = attr.lastModifiedTime().toMillis(); 449 | if (modifyTime != metarFileTime) { 450 | buildList(); 451 | } 452 | } catch (IOException e) { 453 | LOGGER.error("[METAR]: Check metar file error."); 454 | } 455 | } 456 | 457 | private int doParse(@NotNull Mmq q) { 458 | Station key = new Station(); 459 | checkMetarFile(); 460 | key.setName(StringUtils.substring(q.getMetarId(), 0, 4).toUpperCase()); 461 | int index = Collections.binarySearch(stationList, key, Comparator.comparing(Station::getName)); 462 | if (index < 0) { 463 | Main.serverInterface.sendNoWx(q.getDestination(), q.getFd(), q.getMetarId()); 464 | delQ(q); 465 | return 1; 466 | } 467 | Station location = stationList.get(index); 468 | 469 | try { 470 | ioOut = new BufferedReader(new InputStreamReader(new FileInputStream(FsdPath.METARFILE))); 471 | } catch (IOException e) { 472 | LOGGER.error("[METAR]: Open metar.txt error. "); 473 | return 0; 474 | } 475 | 476 | String line; 477 | 478 | try { 479 | ioOut.skip(location.getLocation()); 480 | boolean first = true; 481 | StringBuilder sb = new StringBuilder(); 482 | do { 483 | String piece = getLine(); 484 | if (piece == null) { 485 | break; 486 | } 487 | boolean addition = piece.startsWith(" "); 488 | if (addition && first) { 489 | break; 490 | } 491 | if (!addition && !first) { 492 | break; 493 | } 494 | first = false; 495 | if (addition) { 496 | sb.append(piece.substring(4)); 497 | } else { 498 | sb.append(piece); 499 | } 500 | } while (true); 501 | line = sb.toString(); 502 | ioOut.close(); 503 | ioOut = null; 504 | } catch (IOException e) { 505 | LOGGER.error("[METAR]: Read metar.txt error."); 506 | return 0; 507 | } 508 | 509 | if (q.isParsed() == 1) { 510 | List arr = new ArrayList<>(); 511 | int count = Support.breakArgs(line, arr, 100); 512 | WProfile profile = new WProfile(location.getName(), 0, null); 513 | profile.parseMetar(arr.toArray(new String[0]), count); 514 | Main.serverInterface.sendWeather(q.getDestination(), q.getFd(), profile); 515 | } else { 516 | Main.serverInterface.sendMetar(q.getDestination(), q.getFd(), location.getName(), line); 517 | } 518 | 519 | Manage.manager.incVar(varTotal); 520 | delQ(q); 521 | return 1; 522 | } 523 | 524 | private @Nullable String getLine() throws IOException { 525 | String line = Objects.requireNonNull(ioOut).readLine(); 526 | if (line == null) { 527 | return null; 528 | } 529 | return prepareLine(line); 530 | } 531 | 532 | private String prepareLine(String line) { 533 | return StringUtils.stripEnd(line, "=\r\n"); 534 | } 535 | 536 | public @Nullable Server requestMetar(@NotNull String client, String metar, int parsed, int fd) { 537 | if (source == MetarSource.SOURCE_NETWORK) { 538 | int hops = -1; 539 | Server best = null; 540 | for (Server tempServer : Server.servers) { 541 | if (tempServer != Server.myServer && ((tempServer.getFlags() & ServerConstants.SERVER_METAR) != 0)) { 542 | if (hops == -1 || (tempServer.getHops() < hops && tempServer.getHops() != -1)) { 543 | best = tempServer; 544 | hops = tempServer.getHops(); 545 | } 546 | } 547 | } 548 | 549 | if (best == null) { 550 | return null; 551 | } 552 | Main.serverInterface.sendReqMetar(client, metar, fd, parsed, best); 553 | return best; 554 | } else { 555 | addQ(client, metar, parsed, fd); 556 | } 557 | return Server.myServer; 558 | } 559 | 560 | private void addQ(@NotNull String dest, String metar, int parsed, int fd) { 561 | WProfile prof = Weather.getWProfile(metar); 562 | if (prof != null && prof.getActive() == 1) { 563 | if (parsed == 1) { 564 | Main.serverInterface.sendWeather(dest, fd, prof); 565 | } else { 566 | Main.serverInterface.sendMetar(dest, fd, metar, prof.getRawCode()); 567 | } 568 | return; 569 | } 570 | Mmq temp = new Mmq(); 571 | temp.setDestination(dest); 572 | temp.setMetarId(metar); 573 | temp.setFd(fd); 574 | temp.setParsed(parsed); 575 | temp.setNext(rootQ); 576 | temp.setPrev(null); 577 | if (temp.getNext() != null) { 578 | rootQ = temp; 579 | } 580 | } 581 | 582 | private void delQ(@NotNull Mmq p) { 583 | if (p.getNext() != null) { 584 | p.getNext().setPrev(p.getPrev()); 585 | } 586 | if (p.getPrev() != null) { 587 | p.getPrev().setNext(p.getNext()); 588 | } else { 589 | rootQ = p.getNext(); 590 | } 591 | } 592 | 593 | public int getVariation(int num, int min, int max) { 594 | int val = variation[num], range = max - min + 1; 595 | val = (Math.abs(val) % range) + min; 596 | return val; 597 | } 598 | 599 | public boolean isDownloading() { 600 | return downloading; 601 | } 602 | 603 | public void setDownloading(boolean downloading) { 604 | this.downloading = downloading; 605 | } 606 | 607 | public int getSource() { 608 | return source; 609 | } 610 | 611 | public void setSource(int source) { 612 | this.source = source; 613 | } 614 | } 615 | --------------------------------------------------------------------------------