fork repo
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				backend / cross (aarch64) (push) Failing after 17s
				
			
		
			
				
	
				backend / cross (armhf) (push) Failing after 31s
				
			
		
			
				
	
				backend / cross (mips) (push) Failing after 31s
				
			
		
			
				
	
				backend / cross (mips64) (push) Failing after 31s
				
			
		
			
				
	
				backend / cross (mips64el) (push) Failing after 31s
				
			
		
			
				
	
				frontend / build (push) Failing after 32s
				
			
		
			
				
	
				backend / cross (arm) (push) Failing after 2m19s
				
			
		
			
				
	
				backend / cross (i686) (push) Failing after 1s
				
			
		
			
				
	
				backend / cross (mipsel) (push) Failing after 31s
				
			
		
			
				
	
				backend / cross (s390x) (push) Failing after 31s
				
			
		
			
				
	
				backend / cross (win32) (push) Failing after 31s
				
			
		
			
				
	
				backend / cross (x86_64) (push) Failing after 32s
				
			
		
			
				
	
				docker / build (push) Failing after 6m14s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	backend / cross (aarch64) (push) Failing after 17s
				
			backend / cross (armhf) (push) Failing after 31s
				
			backend / cross (mips) (push) Failing after 31s
				
			backend / cross (mips64) (push) Failing after 31s
				
			backend / cross (mips64el) (push) Failing after 31s
				
			frontend / build (push) Failing after 32s
				
			backend / cross (arm) (push) Failing after 2m19s
				
			backend / cross (i686) (push) Failing after 1s
				
			backend / cross (mipsel) (push) Failing after 31s
				
			backend / cross (s390x) (push) Failing after 31s
				
			backend / cross (win32) (push) Failing after 31s
				
			backend / cross (x86_64) (push) Failing after 32s
				
			docker / build (push) Failing after 6m14s
				
			This commit is contained in:
		
							
								
								
									
										6
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
BasedOnStyle: Google
 | 
			
		||||
Language: Cpp
 | 
			
		||||
ColumnLimit: 120
 | 
			
		||||
IndentWidth: 2
 | 
			
		||||
TabWidth: 2
 | 
			
		||||
UseTab: Never
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
src/html.h linguist-generated
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
github: tsl0922
 | 
			
		||||
patreon: tsl0922
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a report to help us improve
 | 
			
		||||
title: ''
 | 
			
		||||
labels: bug
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
A clear and concise description of what the bug is.
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Click on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
 | 
			
		||||
**Expected behavior**
 | 
			
		||||
A clear and concise description of what you expected to happen.
 | 
			
		||||
 | 
			
		||||
**Screenshots**
 | 
			
		||||
If applicable, add screenshots to help explain your problem.
 | 
			
		||||
 | 
			
		||||
**Environment:**
 | 
			
		||||
 - OS: [e.g. macOS 10.15.2]
 | 
			
		||||
 - Browser: [e.g. Chrome 79.0.3945.130]
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context about the problem here.
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
title: ''
 | 
			
		||||
labels: enhancement
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Is your feature request related to a problem? Please describe.**
 | 
			
		||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
A clear and concise description of what you want to happen.
 | 
			
		||||
 | 
			
		||||
**Describe alternatives you've considered**
 | 
			
		||||
A clear and concise description of any alternative solutions or features you've considered.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context or screenshots about the feature request here.
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/ISSUE_TEMPLATE/support-request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.github/ISSUE_TEMPLATE/support-request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
---
 | 
			
		||||
name: Support Request
 | 
			
		||||
about: Support request or question
 | 
			
		||||
title: ''
 | 
			
		||||
labels: question
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Describe your problem or question here.
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
version: 2
 | 
			
		||||
updates:
 | 
			
		||||
- package-ecosystem: npm
 | 
			
		||||
  directory: "/html"
 | 
			
		||||
  schedule:
 | 
			
		||||
    interval: daily
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
							
								
								
									
										39
									
								
								.github/workflows/backend.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/backend.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
name: backend
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    paths:
 | 
			
		||||
    - ".github/workflows/backend.yml"
 | 
			
		||||
    - "CMakeLists.txt"
 | 
			
		||||
    - "src/*"
 | 
			
		||||
    - "scripts/*"
 | 
			
		||||
  pull_request:
 | 
			
		||||
    paths:
 | 
			
		||||
    - ".github/workflows/backend.yml"
 | 
			
		||||
    - "CMakeLists.txt"
 | 
			
		||||
    - "src/*"
 | 
			
		||||
    - "scripts/*"
 | 
			
		||||
  workflow_call:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  cross:
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        target: [i686, x86_64, arm, armhf, aarch64, mips, mipsel, mips64, mips64el, s390x, win32]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Install packages
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get update
 | 
			
		||||
          sudo apt-get install -y autoconf automake build-essential cmake curl file libtool
 | 
			
		||||
      - name: Cross build (${{ matrix.target }})
 | 
			
		||||
        env:
 | 
			
		||||
          BUILD_TARGET: ${{ matrix.target }}
 | 
			
		||||
        run: ./scripts/cross-build.sh
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: ttyd.${{ matrix.target }}
 | 
			
		||||
          path: build/ttyd*
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										71
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
name: docker
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: main
 | 
			
		||||
    tags: ["*"]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write
 | 
			
		||||
      contents: read
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Install packages
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get update
 | 
			
		||||
          sudo apt-get install -y autoconf automake build-essential cmake curl file libtool
 | 
			
		||||
      - name: Cross build multi-arch binary
 | 
			
		||||
        run: |
 | 
			
		||||
          mkdir dist
 | 
			
		||||
          for arch in amd64 armv7 arm64 s390x; do
 | 
			
		||||
            env BUILD_TARGET=$arch ./scripts/cross-build.sh
 | 
			
		||||
            [ "$arch" = "armv7" ] && arch="arm" 
 | 
			
		||||
            mkdir -p dist/$arch && cp build/ttyd dist/$arch/ttyd
 | 
			
		||||
          done
 | 
			
		||||
      - uses: docker/setup-qemu-action@v3
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_HUB_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_HUB_TOKEN }}
 | 
			
		||||
      - uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      - name: Determine docker tags
 | 
			
		||||
        id: docker_tag
 | 
			
		||||
        run: |
 | 
			
		||||
          case $GITHUB_REF in
 | 
			
		||||
            refs/tags/*)
 | 
			
		||||
              TAG_NAME=${GITHUB_REF#refs/tags/}
 | 
			
		||||
              echo "DOCKER_TAG=tsl0922/ttyd:${TAG_NAME}" >> $GITHUB_ENV
 | 
			
		||||
              echo "ALPINE_TAG=tsl0922/ttyd:${TAG_NAME}-alpine" >> $GITHUB_ENV
 | 
			
		||||
              ;;
 | 
			
		||||
            *)
 | 
			
		||||
              echo "DOCKER_TAG=tsl0922/ttyd:latest" >> $GITHUB_ENV
 | 
			
		||||
              echo "ALPINE_TAG=tsl0922/ttyd:alpine" >> $GITHUB_ENV
 | 
			
		||||
          esac
 | 
			
		||||
      - name: build/push docker image
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          file: ./Dockerfile
 | 
			
		||||
          platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/s390x
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: |
 | 
			
		||||
            ${{ env.DOCKER_TAG }}
 | 
			
		||||
            ghcr.io/${{ env.DOCKER_TAG }}
 | 
			
		||||
      - name: build/push docker image (alpine)
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          file: ./Dockerfile.alpine
 | 
			
		||||
          platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/s390x
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: |
 | 
			
		||||
            ${{ env.ALPINE_TAG }}
 | 
			
		||||
            ghcr.io/${{ env.ALPINE_TAG }}
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/frontend.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/frontend.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
name: frontend
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    paths:
 | 
			
		||||
    - ".github/workflows/frontend.yml"
 | 
			
		||||
    - "html/*"
 | 
			
		||||
  pull_request:
 | 
			
		||||
    paths:
 | 
			
		||||
    - ".github/workflows/frontend.yml"
 | 
			
		||||
    - "html/*"
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
      - name: Run yarn install, check and build
 | 
			
		||||
        run: |
 | 
			
		||||
          corepack enable
 | 
			
		||||
          corepack prepare yarn@stable --activate
 | 
			
		||||
          yarn install
 | 
			
		||||
          yarn run check
 | 
			
		||||
          yarn run build
 | 
			
		||||
        working-directory: html
 | 
			
		||||
							
								
								
									
										37
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
name: release
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags: ["*"]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    uses: ./.github/workflows/backend.yml
 | 
			
		||||
  publish:
 | 
			
		||||
    needs: [build]
 | 
			
		||||
    runs-on: ubuntu-22.04
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Check version bump
 | 
			
		||||
        run: |
 | 
			
		||||
          TAG=$(git describe --tags --match "[0-9]*.[0-9]*.[0-9]*" --abbrev=8)
 | 
			
		||||
          VERSION=$(grep project CMakeLists.txt| awk '{print $3}')
 | 
			
		||||
          if [ "$TAG" != "$VERSION" ]; then
 | 
			
		||||
            echo "=== Version in CMakeLists.txt and git tag does not match!"
 | 
			
		||||
            echo "=== Git Tag: $TAG, Version: $VERSION"
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
      - run: |
 | 
			
		||||
          mkdir build
 | 
			
		||||
          for file in ttyd.*/*; do
 | 
			
		||||
            target=$(echo $file | awk -F/ '{print $1}')
 | 
			
		||||
            [[ $file == *.exe ]] && target="$target.exe"
 | 
			
		||||
            mv $file build/$target
 | 
			
		||||
          done
 | 
			
		||||
          pushd build; sha256sum ttyd.* > SHA256SUMS; popd
 | 
			
		||||
      - uses: ncipollo/release-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          artifacts: build/*
 | 
			
		||||
          allowUpdates: true
 | 
			
		||||
          draft: true
 | 
			
		||||
							
								
								
									
										54
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
# Prerequisites
 | 
			
		||||
*.d
 | 
			
		||||
 | 
			
		||||
# Object files
 | 
			
		||||
*.o
 | 
			
		||||
*.ko
 | 
			
		||||
*.obj
 | 
			
		||||
*.elf
 | 
			
		||||
 | 
			
		||||
# Precompiled Headers
 | 
			
		||||
*.gch
 | 
			
		||||
*.pch
 | 
			
		||||
 | 
			
		||||
# Libraries
 | 
			
		||||
*.lib
 | 
			
		||||
*.a
 | 
			
		||||
*.la
 | 
			
		||||
*.lo
 | 
			
		||||
 | 
			
		||||
# Shared objects (inc. Windows DLLs)
 | 
			
		||||
*.dll
 | 
			
		||||
*.so
 | 
			
		||||
*.so.*
 | 
			
		||||
*.dylib
 | 
			
		||||
 | 
			
		||||
# Executables
 | 
			
		||||
*.exe
 | 
			
		||||
*.out
 | 
			
		||||
*.app
 | 
			
		||||
*.i*86
 | 
			
		||||
*.x86_64
 | 
			
		||||
*.hex
 | 
			
		||||
 | 
			
		||||
# Debug files
 | 
			
		||||
*.dSYM/
 | 
			
		||||
*.su
 | 
			
		||||
 | 
			
		||||
# Cmake files
 | 
			
		||||
CMakeCache.txt
 | 
			
		||||
CMakeFiles
 | 
			
		||||
CMakeScripts
 | 
			
		||||
cmake_install.cmake
 | 
			
		||||
install_manifest.txt
 | 
			
		||||
CTestTestfile.cmake
 | 
			
		||||
build
 | 
			
		||||
 | 
			
		||||
# Clion files
 | 
			
		||||
.idea/
 | 
			
		||||
 | 
			
		||||
# VSCode files
 | 
			
		||||
.vscode/
 | 
			
		||||
 | 
			
		||||
# Project files
 | 
			
		||||
!init.d
 | 
			
		||||
							
								
								
									
										94
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
cmake_minimum_required(VERSION 3.12.0)
 | 
			
		||||
 | 
			
		||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 | 
			
		||||
 | 
			
		||||
project(ttyd VERSION 1.7.7 LANGUAGES C)
 | 
			
		||||
 | 
			
		||||
set(TTYD_VERSION "${PROJECT_VERSION}")
 | 
			
		||||
 | 
			
		||||
include(GetGitVersion)
 | 
			
		||||
get_git_version(GIT_VERSION SEM_VER)
 | 
			
		||||
get_git_head(GIT_COMMIT)
 | 
			
		||||
if("${SEM_VER}" VERSION_GREATER "${TTYD_VERSION}")
 | 
			
		||||
    set(TTYD_VERSION "${SEM_VER}")
 | 
			
		||||
endif()
 | 
			
		||||
if(NOT "${GIT_COMMIT}" STREQUAL "")
 | 
			
		||||
    set(TTYD_VERSION "${TTYD_VERSION}-${GIT_COMMIT}")
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE")
 | 
			
		||||
if(CMAKE_VERSION VERSION_LESS "3.1")
 | 
			
		||||
    if ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
 | 
			
		||||
        set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99")
 | 
			
		||||
    else()
 | 
			
		||||
        set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
 | 
			
		||||
    endif()
 | 
			
		||||
else()
 | 
			
		||||
    set(CMAKE_C_STANDARD 99)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
set(SOURCE_FILES src/utils.c src/pty.c src/protocol.c src/http.c src/server.c)
 | 
			
		||||
 | 
			
		||||
include(FindPackageHandleStandardArgs)
 | 
			
		||||
 | 
			
		||||
find_path(LIBUV_INCLUDE_DIR NAMES uv.h)
 | 
			
		||||
find_library(LIBUV_LIBRARY NAMES uv libuv)
 | 
			
		||||
find_package_handle_standard_args(LIBUV REQUIRED_VARS LIBUV_LIBRARY LIBUV_INCLUDE_DIR)
 | 
			
		||||
mark_as_advanced(LIBUV_INCLUDE_DIR LIBUV_LIBRARY)
 | 
			
		||||
if(LIBUV_FOUND)
 | 
			
		||||
    SET(LIBUV_INCLUDE_DIRS "${LIBUV_INCLUDE_DIR}")
 | 
			
		||||
    SET(LIBUV_LIBRARIES "${LIBUV_LIBRARY}")
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
find_path(JSON-C_INCLUDE_DIR NAMES json.h PATH_SUFFIXES json-c)
 | 
			
		||||
find_library(JSON-C_LIBRARY NAMES json-c)
 | 
			
		||||
find_package_handle_standard_args(JSON-C REQUIRED_VARS JSON-C_LIBRARY JSON-C_INCLUDE_DIR)
 | 
			
		||||
mark_as_advanced(JSON-C_INCLUDE_DIR JSON-C_LIBRARY)
 | 
			
		||||
if(JSON-C_FOUND)
 | 
			
		||||
    SET(JSON-C_INCLUDE_DIRS "${JSON-C_INCLUDE_DIR}")
 | 
			
		||||
    SET(JSON-C_LIBRARIES "${JSON-C_LIBRARY}")
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
find_package(ZLIB REQUIRED)
 | 
			
		||||
find_package(Libwebsockets 3.2.0 REQUIRED)
 | 
			
		||||
 | 
			
		||||
set(INCLUDE_DIRS ${ZLIB_INCLUDE_DIR} ${LIBWEBSOCKETS_INCLUDE_DIRS} ${JSON-C_INCLUDE_DIRS} ${LIBUV_INCLUDE_DIRS})
 | 
			
		||||
set(LINK_LIBS ${ZLIB_LIBRARIES} ${LIBWEBSOCKETS_LIBRARIES} ${JSON-C_LIBRARIES} ${LIBUV_LIBRARIES})
 | 
			
		||||
 | 
			
		||||
set (CMAKE_REQUIRED_INCLUDES ${INCLUDE_DIRS})
 | 
			
		||||
include(CheckSymbolExists)
 | 
			
		||||
check_symbol_exists(LWS_WITH_LIBUV "lws_config.h" LWS_WITH_LIBUV)
 | 
			
		||||
check_symbol_exists(LWS_OPENSSL_SUPPORT "lws_config.h" LWS_OPENSSL_ENABLED)
 | 
			
		||||
check_symbol_exists(LWS_WITH_MBEDTLS "lws_config.h" LWS_MBEDTLS_ENABLED)
 | 
			
		||||
if(NOT LWS_WITH_LIBUV)
 | 
			
		||||
    message(FATAL_ERROR "libwebsockets was not build with libuv support (-DLWS_WITH_LIBUV=ON)")
 | 
			
		||||
endif()
 | 
			
		||||
if(LWS_OPENSSL_ENABLED AND NOT LWS_MBEDTLS_ENABLED)
 | 
			
		||||
    find_package(OpenSSL REQUIRED)
 | 
			
		||||
    list(APPEND INCLUDE_DIRS ${OPENSSL_INCLUDE_DIR})
 | 
			
		||||
    list(APPEND LINK_LIBS ${OPENSSL_LIBRARIES})
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
if(WIN32)
 | 
			
		||||
    list(APPEND LINK_LIBS shell32 ws2_32)
 | 
			
		||||
    configure_file(${CMAKE_CURRENT_SOURCE_DIR}/app.rc.in ${CMAKE_CURRENT_BINARY_DIR}/app.rc @ONLY)
 | 
			
		||||
    list(APPEND SOURCE_FILES ${CMAKE_CURRENT_BINARY_DIR}/app.rc)
 | 
			
		||||
else()
 | 
			
		||||
    find_library(LIBUTIL NAMES util)
 | 
			
		||||
    if(LIBUTIL)
 | 
			
		||||
        list(APPEND LINK_LIBS util)
 | 
			
		||||
    endif()
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
 | 
			
		||||
target_include_directories(${PROJECT_NAME} PUBLIC ${INCLUDE_DIRS})
 | 
			
		||||
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
 | 
			
		||||
target_compile_definitions(${PROJECT_NAME} PUBLIC
 | 
			
		||||
    TTYD_VERSION="${TTYD_VERSION}"
 | 
			
		||||
    $<$<PLATFORM_ID:Windows>:_WIN32_WINNT=0xa00 WINVER=0xa00>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
include(GNUInstallDirs)
 | 
			
		||||
 | 
			
		||||
install(TARGETS ${PROJECT_NAME} DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT prog)
 | 
			
		||||
install(FILES man/ttyd.1 DESTINATION "${CMAKE_INSTALL_MANDIR}/man1" COMPONENT doc)
 | 
			
		||||
							
								
								
									
										15
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
FROM ubuntu:20.04
 | 
			
		||||
 | 
			
		||||
ARG TARGETARCH
 | 
			
		||||
 | 
			
		||||
# Dependencies
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Application
 | 
			
		||||
COPY ./dist/${TARGETARCH}/ttyd /usr/bin/ttyd
 | 
			
		||||
 | 
			
		||||
EXPOSE 7681
 | 
			
		||||
WORKDIR /root
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/usr/bin/tini", "--"]
 | 
			
		||||
CMD ["ttyd", "-W", "bash"]
 | 
			
		||||
							
								
								
									
										15
									
								
								Dockerfile.alpine
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Dockerfile.alpine
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
FROM alpine
 | 
			
		||||
 | 
			
		||||
ARG TARGETARCH
 | 
			
		||||
 | 
			
		||||
# Dependencies
 | 
			
		||||
RUN apk add --no-cache bash tini
 | 
			
		||||
 | 
			
		||||
# Application
 | 
			
		||||
COPY ./dist/${TARGETARCH}/ttyd /usr/bin/ttyd
 | 
			
		||||
 | 
			
		||||
EXPOSE 7681
 | 
			
		||||
WORKDIR /root
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/sbin/tini", "--"]
 | 
			
		||||
CMD ["ttyd", "-W", "bash"]
 | 
			
		||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2016-2025 Shuanglei Tao <tsl0922@gmail.com>
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
							
								
								
									
										108
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
[](https://github.com/tsl0922/ttyd/releases)
 | 
			
		||||
[](https://hub.docker.com/r/tsl0922/ttyd)
 | 
			
		||||
[](https://repology.org/project/ttyd/versions)
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
# ttyd - Share your terminal over the web
 | 
			
		||||
 | 
			
		||||
ttyd is a simple command-line tool for sharing terminal over the web.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
# Features
 | 
			
		||||
 | 
			
		||||
- Built on top of [libuv](https://libuv.org) and [WebGL2](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) for speed
 | 
			
		||||
- Fully-featured terminal with [CJK](https://en.wikipedia.org/wiki/CJK_characters) and IME support
 | 
			
		||||
- [ZMODEM](https://en.wikipedia.org/wiki/ZMODEM) ([lrzsz](https://ohse.de/uwe/software/lrzsz.html)) / [trzsz](https://trzsz.github.io) file transfer support
 | 
			
		||||
- [Sixel](https://en.wikipedia.org/wiki/Sixel) image output support ([img2sixel](https://saitoha.github.io/libsixel) / [lsix](https://github.com/hackerb9/lsix))
 | 
			
		||||
- SSL support based on [OpenSSL](https://www.openssl.org) / [Mbed TLS](https://github.com/Mbed-TLS/mbedtls)
 | 
			
		||||
- Run any custom command with options
 | 
			
		||||
- Basic authentication support and many other custom options
 | 
			
		||||
- Cross platform: macOS, Linux, FreeBSD/OpenBSD, [OpenWrt](https://openwrt.org), Windows
 | 
			
		||||
 | 
			
		||||
> ❤ Special thanks to [JetBrains](https://www.jetbrains.com/?from=ttyd) for sponsoring the opensource license to this project.
 | 
			
		||||
 | 
			
		||||
# Installation
 | 
			
		||||
 | 
			
		||||
## Install on macOS
 | 
			
		||||
 | 
			
		||||
- Install with [Homebrew](http://brew.sh): `brew install ttyd`
 | 
			
		||||
- Install with [MacPorts](https://www.macports.org): `sudo port install ttyd`
 | 
			
		||||
 | 
			
		||||
## Install on Linux
 | 
			
		||||
 | 
			
		||||
- Binary version (recommended): download from the [releases](https://github.com/tsl0922/ttyd/releases) page
 | 
			
		||||
- Install with [Homebrew](https://docs.brew.sh/Homebrew-on-Linux) : `brew install ttyd`
 | 
			
		||||
- Install the snap: `sudo snap install ttyd --classic`
 | 
			
		||||
- Build from source (debian/ubuntu):
 | 
			
		||||
    ```bash
 | 
			
		||||
    sudo apt-get update
 | 
			
		||||
    sudo apt-get install -y build-essential cmake git libjson-c-dev libwebsockets-dev
 | 
			
		||||
    git clone https://github.com/tsl0922/ttyd.git
 | 
			
		||||
    cd ttyd && mkdir build && cd build
 | 
			
		||||
    cmake ..
 | 
			
		||||
    make && sudo make install
 | 
			
		||||
    ```
 | 
			
		||||
    You may also need to compile/install [libwebsockets](https://libwebsockets.org) from source if the `libwebsockets-dev` package is outdated.
 | 
			
		||||
- Install on OpenWrt: `opkg install ttyd`
 | 
			
		||||
- Install on Gentoo: clone the [repo](https://bitbucket.org/mgpagano/ttyd/src/master) and follow the directions [here](https://wiki.gentoo.org/wiki/Custom_repository#Creating_a_local_repository).
 | 
			
		||||
 | 
			
		||||
## Install on Windows
 | 
			
		||||
 | 
			
		||||
- Binary version (recommended): download from the [releases](https://github.com/tsl0922/ttyd/releases) page
 | 
			
		||||
- Install with [WinGet](https://github.com/microsoft/winget-cli): `winget install tsl0922.ttyd`
 | 
			
		||||
- Install with [Scoop](https://scoop.sh/#/apps?q=ttyd&s=2&d=1&o=true): `scoop install ttyd`
 | 
			
		||||
- [Compile on Windows](https://github.com/tsl0922/ttyd/wiki/Compile-on-Windows)
 | 
			
		||||
 | 
			
		||||
# Usage
 | 
			
		||||
 | 
			
		||||
## Command-line Options
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
USAGE:
 | 
			
		||||
    ttyd [options] <command> [<arguments...>]
 | 
			
		||||
 | 
			
		||||
OPTIONS:
 | 
			
		||||
    -p, --port              Port to listen (default: 7681, use `0` for random port)
 | 
			
		||||
    -i, --interface         Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
 | 
			
		||||
    -U, --socket-owner      User owner of the UNIX domain socket file, when enabled (eg: user:group)
 | 
			
		||||
    -c, --credential        Credential for basic authentication (format: username:password)
 | 
			
		||||
    -H, --auth-header       HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication
 | 
			
		||||
    -u, --uid               User id to run with
 | 
			
		||||
    -g, --gid               Group id to run with
 | 
			
		||||
    -s, --signal            Signal to send to the command when exit it (default: 1, SIGHUP)
 | 
			
		||||
    -w, --cwd               Working directory to be set for the child program
 | 
			
		||||
    -a, --url-arg           Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
 | 
			
		||||
    -W, --writable          Allow clients to write to the TTY (readonly by default)
 | 
			
		||||
    -t, --client-option     Send option to client (format: key=value), repeat to add more options
 | 
			
		||||
    -T, --terminal-type     Terminal type to report, default: xterm-256color
 | 
			
		||||
    -O, --check-origin      Do not allow websocket connection from different origin
 | 
			
		||||
    -m, --max-clients       Maximum clients to support (default: 0, no limit)
 | 
			
		||||
    -o, --once              Accept only one client and exit on disconnection
 | 
			
		||||
    -q, --exit-no-conn      Exit on all clients disconnection
 | 
			
		||||
    -B, --browser           Open terminal with the default system browser
 | 
			
		||||
    -I, --index             Custom index.html path
 | 
			
		||||
    -b, --base-path         Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)
 | 
			
		||||
    -P, --ping-interval     Websocket ping interval(sec) (default: 5)
 | 
			
		||||
    -6, --ipv6              Enable IPv6 support
 | 
			
		||||
    -S, --ssl               Enable SSL
 | 
			
		||||
    -C, --ssl-cert          SSL certificate file path
 | 
			
		||||
    -K, --ssl-key           SSL key file path
 | 
			
		||||
    -A, --ssl-ca            SSL CA file path for client certificate verification
 | 
			
		||||
    -d, --debug             Set log level (default: 7)
 | 
			
		||||
    -v, --version           Print the version and exit
 | 
			
		||||
    -h, --help              Print this text and exit
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Read the example usage on the [wiki](https://github.com/tsl0922/ttyd/wiki/Example-Usage).
 | 
			
		||||
 | 
			
		||||
## Browser Support
 | 
			
		||||
 | 
			
		||||
Modern browsers, See [Browser Support](https://github.com/xtermjs/xterm.js#browser-support).
 | 
			
		||||
 | 
			
		||||
## Alternatives
 | 
			
		||||
 | 
			
		||||
* [Wetty](https://github.com/krishnasrinivas/wetty): [Node](https://nodejs.org) based web terminal (SSH/login)
 | 
			
		||||
* [GoTTY](https://github.com/yudai/gotty): [Go](https://golang.org) based web terminal
 | 
			
		||||
							
								
								
									
										32
									
								
								app.rc.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app.rc.in
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
#include <winver.h>
 | 
			
		||||
 | 
			
		||||
#define VERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
 | 
			
		||||
#define VERSION_STR "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0\0"
 | 
			
		||||
 | 
			
		||||
VS_VERSION_INFO VERSIONINFO
 | 
			
		||||
 FILEVERSION VERSION
 | 
			
		||||
 PRODUCTVERSION VERSION
 | 
			
		||||
 FILEFLAGSMASK  VS_FFI_FILEFLAGSMASK
 | 
			
		||||
 FILEFLAGS      0
 | 
			
		||||
 FILEOS         VOS__WINDOWS32
 | 
			
		||||
 FILETYPE       VFT_DLL
 | 
			
		||||
BEGIN
 | 
			
		||||
    BLOCK "StringFileInfo"
 | 
			
		||||
    BEGIN
 | 
			
		||||
        BLOCK "040904b0"
 | 
			
		||||
        BEGIN
 | 
			
		||||
            VALUE "FileDescription", "ttyd\0"
 | 
			
		||||
            VALUE "ProductName", "ttyd\0"
 | 
			
		||||
            VALUE "ProductVersion", VERSION_STR
 | 
			
		||||
            VALUE "FileVersion", VERSION_STR
 | 
			
		||||
            VALUE "InternalName", "ttyd\0"
 | 
			
		||||
            VALUE "OriginalFilename", "ttyd.exe\0"
 | 
			
		||||
            VALUE "LegalCopyright", "Copyright (C) 2016-2025 Shuanglei Tao\0"
 | 
			
		||||
        VALUE "Comment", "\0"
 | 
			
		||||
        END
 | 
			
		||||
    END
 | 
			
		||||
    BLOCK "VarFileInfo"
 | 
			
		||||
    BEGIN
 | 
			
		||||
        VALUE "Translation", 0x409, 1200
 | 
			
		||||
    END
 | 
			
		||||
END
 | 
			
		||||
							
								
								
									
										47
									
								
								cmake/GetGitVersion.cmake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								cmake/GetGitVersion.cmake
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
find_package(Git)
 | 
			
		||||
 | 
			
		||||
function(get_git_version var1 var2)
 | 
			
		||||
  if(GIT_EXECUTABLE)
 | 
			
		||||
    execute_process(
 | 
			
		||||
      COMMAND ${GIT_EXECUTABLE} describe --tags --match "[0-9]*.[0-9]*.[0-9]*" --abbrev=8
 | 
			
		||||
      WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
 | 
			
		||||
      RESULT_VARIABLE status
 | 
			
		||||
      OUTPUT_VARIABLE GIT_VERSION
 | 
			
		||||
    )
 | 
			
		||||
    if (${status})
 | 
			
		||||
      set(GIT_VERSION "0.0.0")
 | 
			
		||||
    else()
 | 
			
		||||
      string(STRIP ${GIT_VERSION} GIT_VERSION)
 | 
			
		||||
      string(REGEX REPLACE "-[0-9]+-g" "-" GIT_VERSION ${GIT_VERSION})
 | 
			
		||||
    endif()
 | 
			
		||||
  else()
 | 
			
		||||
    set(GIT_VERSION "0.0.0")
 | 
			
		||||
  endif()
 | 
			
		||||
 | 
			
		||||
  string(REGEX MATCH "^[0-9]+.[0-9]+.[0-9]+" SEM_VER "${GIT_VERSION}")
 | 
			
		||||
 | 
			
		||||
  message("-- Git Tag: ${GIT_VERSION}, Sem Ver: ${SEM_VER}")
 | 
			
		||||
 | 
			
		||||
  set(${var1} ${GIT_VERSION} PARENT_SCOPE)
 | 
			
		||||
  set(${var2} ${SEM_VER} PARENT_SCOPE)
 | 
			
		||||
endfunction()
 | 
			
		||||
 | 
			
		||||
function(get_git_head var1)
 | 
			
		||||
  if(GIT_EXECUTABLE)
 | 
			
		||||
    execute_process(
 | 
			
		||||
      COMMAND ${GIT_EXECUTABLE} --git-dir ${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse --short HEAD
 | 
			
		||||
      RESULT_VARIABLE status
 | 
			
		||||
      OUTPUT_VARIABLE GIT_COMMIT
 | 
			
		||||
      OUTPUT_STRIP_TRAILING_WHITESPACE
 | 
			
		||||
      ERROR_QUIET
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if(${status})
 | 
			
		||||
      set(GIT_COMMIT "unknown")
 | 
			
		||||
    endif()
 | 
			
		||||
 | 
			
		||||
    message("-- Git Commit: ${GIT_COMMIT}")
 | 
			
		||||
 | 
			
		||||
    set(${var1} ${GIT_COMMIT} PARENT_SCOPE)
 | 
			
		||||
  endif()
 | 
			
		||||
endfunction()
 | 
			
		||||
							
								
								
									
										14
									
								
								html/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								html/.editorconfig
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
indent_size = 4
 | 
			
		||||
indent_style = space
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
 | 
			
		||||
[{*.json, *.scss}]
 | 
			
		||||
indent_size = 2
 | 
			
		||||
 | 
			
		||||
[*.md]
 | 
			
		||||
trim_trailing_whitespace = false
 | 
			
		||||
							
								
								
									
										2
									
								
								html/.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								html/.eslintignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
dist/
 | 
			
		||||
src/
 | 
			
		||||
							
								
								
									
										20
									
								
								html/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								html/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "./node_modules/gts/",
 | 
			
		||||
  "overrides": [
 | 
			
		||||
    {
 | 
			
		||||
      "files": ["**/*.ts", "**/*.tsx"],
 | 
			
		||||
      "parserOptions": {
 | 
			
		||||
        "jsxPragma": "h"
 | 
			
		||||
      },
 | 
			
		||||
      "rules": {
 | 
			
		||||
        "@typescript-eslint/no-duplicate-enum-values": "off"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "files": ["gulpfile.js", "webpack.config.js"],
 | 
			
		||||
      "rules": {
 | 
			
		||||
        "node/no-unpublished-require": "off"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								html/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								html/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
*.log
 | 
			
		||||
 | 
			
		||||
.yarn/cache
 | 
			
		||||
.yarn/unplugged
 | 
			
		||||
.yarn/build-state.yml
 | 
			
		||||
.yarn/install-state.gz
 | 
			
		||||
.pnp.*
 | 
			
		||||
							
								
								
									
										6
									
								
								html/.prettierrc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								html/.prettierrc.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  ...require('gts/.prettierrc.json'),
 | 
			
		||||
  "bracketSpacing": true,
 | 
			
		||||
  "tabWidth": 4,
 | 
			
		||||
  "printWidth": 120,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								html/.yarn/patches/zmodem.js-npm-0.1.10-e5537fa2ed.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								html/.yarn/patches/zmodem.js-npm-0.1.10-e5537fa2ed.patch
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
diff --git a/src/zsession.js b/src/zsession.js
 | 
			
		||||
index 5f0b8f9d8afa6fba0acd6dd0477afa186f7aad9a..c7ea98e0f08c97d63d321f784a5dd8bf66888743 100644
 | 
			
		||||
--- a/src/zsession.js
 | 
			
		||||
+++ b/src/zsession.js
 | 
			
		||||
@@ -548,20 +548,17 @@ Zmodem.Session.Receive = class ZmodemReceiveSession extends Zmodem.Session {
 | 
			
		||||
         if (this._got_ZFIN) {
 | 
			
		||||
             if (this._input_buffer.length < 2) return;
 | 
			
		||||
 
 | 
			
		||||
-            //if it’s OO, then set this._bytes_after_OO
 | 
			
		||||
-            if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) === 0) {
 | 
			
		||||
+            if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) !== 0) {
 | 
			
		||||
+                console.warn( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() );
 | 
			
		||||
+            }
 | 
			
		||||
 
 | 
			
		||||
-                //This doubles as an indication that the session has ended.
 | 
			
		||||
-                //We need to set this right away so that handlers like
 | 
			
		||||
-                //"session_end" will have access to it.
 | 
			
		||||
-                this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0));
 | 
			
		||||
-                this._on_session_end();
 | 
			
		||||
+            //This doubles as an indication that the session has ended.
 | 
			
		||||
+            //We need to set this right away so that handlers like
 | 
			
		||||
+            //"session_end" will have access to it.
 | 
			
		||||
+            this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0));
 | 
			
		||||
+            this._on_session_end();
 | 
			
		||||
 
 | 
			
		||||
-                return;
 | 
			
		||||
-            }
 | 
			
		||||
-            else {
 | 
			
		||||
-                throw( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() );
 | 
			
		||||
-            }
 | 
			
		||||
+            return;
 | 
			
		||||
         }
 | 
			
		||||
 
 | 
			
		||||
         var parsed;
 | 
			
		||||
							
								
								
									
										1
									
								
								html/.yarnrc.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								html/.yarnrc.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
nodeLinker: node-modules
 | 
			
		||||
							
								
								
									
										14
									
								
								html/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								html/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
## Prerequisites
 | 
			
		||||
 | 
			
		||||
> **NOTE:** yarn v2 is required.
 | 
			
		||||
 | 
			
		||||
Install [Yarn](https://yarnpkg.com/getting-started/install), and run: `yarn install`.
 | 
			
		||||
 | 
			
		||||
## Development
 | 
			
		||||
 | 
			
		||||
1. Start ttyd: `ttyd bash`
 | 
			
		||||
2. Start the dev server: `yarn run start`
 | 
			
		||||
 | 
			
		||||
## Publish
 | 
			
		||||
 | 
			
		||||
Run `yarn run build`, this will compile the inlined html to `../src/html.h`.
 | 
			
		||||
							
								
								
									
										68
									
								
								html/gulpfile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								html/gulpfile.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
const { src, dest, task, series } = require('gulp');
 | 
			
		||||
const clean = require('gulp-clean');
 | 
			
		||||
const gzip = require('gulp-gzip');
 | 
			
		||||
const inlineSource = require('gulp-inline-source');
 | 
			
		||||
const rename = require('gulp-rename');
 | 
			
		||||
const through2 = require('through2');
 | 
			
		||||
 | 
			
		||||
const genHeader = (size, buf, len) => {
 | 
			
		||||
    let idx = 0;
 | 
			
		||||
    let data = 'unsigned char index_html[] = {\n  ';
 | 
			
		||||
 | 
			
		||||
    for (const value of buf) {
 | 
			
		||||
        idx++;
 | 
			
		||||
 | 
			
		||||
        const current = value < 0 ? value + 256 : value;
 | 
			
		||||
 | 
			
		||||
        data += '0x';
 | 
			
		||||
        data += (current >>> 4).toString(16);
 | 
			
		||||
        data += (current & 0xf).toString(16);
 | 
			
		||||
 | 
			
		||||
        if (idx === len) {
 | 
			
		||||
            data += '\n';
 | 
			
		||||
        } else {
 | 
			
		||||
            data += idx % 12 === 0 ? ',\n  ' : ', ';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data += '};\n';
 | 
			
		||||
    data += `unsigned int index_html_len = ${len};\n`;
 | 
			
		||||
    data += `unsigned int index_html_size = ${size};\n`;
 | 
			
		||||
    return data;
 | 
			
		||||
};
 | 
			
		||||
let fileSize = 0;
 | 
			
		||||
 | 
			
		||||
task('clean', () => {
 | 
			
		||||
    return src('dist', { read: false, allowEmpty: true }).pipe(clean());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
task('inline', () => {
 | 
			
		||||
    const options = {
 | 
			
		||||
        compress: false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return src('dist/index.html').pipe(inlineSource(options)).pipe(rename('inline.html')).pipe(dest('dist/'));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
task(
 | 
			
		||||
    'default',
 | 
			
		||||
    series('inline', () => {
 | 
			
		||||
        return src('dist/inline.html')
 | 
			
		||||
            .pipe(
 | 
			
		||||
                through2.obj((file, enc, cb) => {
 | 
			
		||||
                    fileSize = file.contents.length;
 | 
			
		||||
                    return cb(null, file);
 | 
			
		||||
                })
 | 
			
		||||
            )
 | 
			
		||||
            .pipe(gzip())
 | 
			
		||||
            .pipe(
 | 
			
		||||
                through2.obj((file, enc, cb) => {
 | 
			
		||||
                    const buf = file.contents;
 | 
			
		||||
                    file.contents = Buffer.from(genHeader(fileSize, buf, buf.length));
 | 
			
		||||
                    return cb(null, file);
 | 
			
		||||
                })
 | 
			
		||||
            )
 | 
			
		||||
            .pipe(rename('html.h'))
 | 
			
		||||
            .pipe(dest('../src/'));
 | 
			
		||||
    })
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										15602
									
								
								html/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15602
									
								
								html/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										74
									
								
								html/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								html/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
{
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "name": "ttyd",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "Share your terminal over the web",
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "url": "git@github.com:tsl0922/ttyd.git",
 | 
			
		||||
    "type": "git"
 | 
			
		||||
  },
 | 
			
		||||
  "author": "Shuanglei Tao <tsl0922@gmail.com>",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "prestart": "gulp clean",
 | 
			
		||||
    "start": "NODE_ENV=development && webpack serve",
 | 
			
		||||
    "build": "NODE_ENV=production webpack && gulp",
 | 
			
		||||
    "inline": "NODE_ENV=production webpack && gulp inline",
 | 
			
		||||
    "check": "gts check",
 | 
			
		||||
    "fix": "gts fix"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=12"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.1.1",
 | 
			
		||||
    "copy-webpack-plugin": "^12.0.2",
 | 
			
		||||
    "css-loader": "^6.10.0",
 | 
			
		||||
    "css-minimizer-webpack-plugin": "^6.0.0",
 | 
			
		||||
    "eslint": "^8.57.0",
 | 
			
		||||
    "eslint-plugin-node": "^11.1.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.1.3",
 | 
			
		||||
    "eslint-webpack-plugin": "^4.0.1",
 | 
			
		||||
    "gts": "^5.2.0",
 | 
			
		||||
    "gulp": "^4.0.2",
 | 
			
		||||
    "gulp-clean": "^0.4.0",
 | 
			
		||||
    "gulp-gzip": "^1.4.2",
 | 
			
		||||
    "gulp-inline-source": "^4.0.0",
 | 
			
		||||
    "gulp-rename": "^2.0.0",
 | 
			
		||||
    "html-webpack-plugin": "^5.6.0",
 | 
			
		||||
    "mini-css-extract-plugin": "^2.8.1",
 | 
			
		||||
    "sass": "^1.71.1",
 | 
			
		||||
    "sass-loader": "^14.1.1",
 | 
			
		||||
    "scssfmt": "^1.0.7",
 | 
			
		||||
    "style-loader": "^3.3.4",
 | 
			
		||||
    "terser-webpack-plugin": "^5.3.10",
 | 
			
		||||
    "through2": "^4.0.2",
 | 
			
		||||
    "ts-loader": "^9.5.1",
 | 
			
		||||
    "typescript": "^5.3.3",
 | 
			
		||||
    "util": "^0.12.5",
 | 
			
		||||
    "webpack": "^5.90.3",
 | 
			
		||||
    "webpack-cli": "^5.1.4",
 | 
			
		||||
    "webpack-dev-server": "^5.0.2",
 | 
			
		||||
    "webpack-merge": "^5.10.0"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@xterm/addon-canvas": "^0.7.0",
 | 
			
		||||
    "@xterm/addon-clipboard": "^0.1.0",
 | 
			
		||||
    "@xterm/addon-fit": "^0.10.0",
 | 
			
		||||
    "@xterm/addon-image": "^0.8.0",
 | 
			
		||||
    "@xterm/addon-unicode11": "^0.8.0",
 | 
			
		||||
    "@xterm/addon-web-links": "^0.11.0",
 | 
			
		||||
    "@xterm/addon-webgl": "^0.18.0",
 | 
			
		||||
    "@xterm/xterm": "^5.5.0",
 | 
			
		||||
    "decko": "^1.2.0",
 | 
			
		||||
    "file-saver": "^2.0.5",
 | 
			
		||||
    "preact": "^10.19.6",
 | 
			
		||||
    "trzsz": "^1.1.5",
 | 
			
		||||
    "whatwg-fetch": "^3.6.20",
 | 
			
		||||
    "zmodem.js": "^0.1.10"
 | 
			
		||||
  },
 | 
			
		||||
  "resolutions": {
 | 
			
		||||
    "zmodem.js@^0.1.10": "patch:zmodem.js@npm%3A0.1.10#./.yarn/patches/zmodem.js-npm-0.1.10-e5537fa2ed.patch"
 | 
			
		||||
  },
 | 
			
		||||
  "packageManager": "yarn@3.6.3"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								html/src/components/app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								html/src/components/app.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import { h, Component } from 'preact';
 | 
			
		||||
 | 
			
		||||
import { Terminal } from './terminal';
 | 
			
		||||
 | 
			
		||||
import type { ITerminalOptions, ITheme } from '@xterm/xterm';
 | 
			
		||||
import type { ClientOptions, FlowControl } from './terminal/xterm';
 | 
			
		||||
 | 
			
		||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
 | 
			
		||||
 | 
			
		||||
const path = window.location.pathname.replace(/[/]+$/, '');
 | 
			
		||||
const wsUrl = [protocol, '//', window.location.host, path,'/ws'].join('');
 | 
			
		||||
const tokenUrl = [window.location.protocol, '//', window.location.host, path,'/token'].join('');
 | 
			
		||||
const clientOptions = {
 | 
			
		||||
    rendererType: 'webgl',
 | 
			
		||||
    disableLeaveAlert: false,
 | 
			
		||||
    disableResizeOverlay: false,
 | 
			
		||||
    enableZmodem: false,
 | 
			
		||||
    enableTrzsz: false,
 | 
			
		||||
    enableSixel: false,
 | 
			
		||||
    closeOnDisconnect: false,
 | 
			
		||||
    isWindows: false,
 | 
			
		||||
    unicodeVersion: '11',
 | 
			
		||||
} as ClientOptions;
 | 
			
		||||
const termOptions = {
 | 
			
		||||
    fontSize: 13,
 | 
			
		||||
    fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace',
 | 
			
		||||
    cursorBlink: true,
 | 
			
		||||
    theme: {
 | 
			
		||||
        foreground: '#d2d2d2',
 | 
			
		||||
        background: '#2b2b2b',
 | 
			
		||||
        cursor: '#adadad',
 | 
			
		||||
        black: '#000000',
 | 
			
		||||
        red: '#d81e00',
 | 
			
		||||
        green: '#5ea702',
 | 
			
		||||
        yellow: '#cfae00',
 | 
			
		||||
        blue: '#427ab3',
 | 
			
		||||
        magenta: '#89658e',
 | 
			
		||||
        cyan: '#00a7aa',
 | 
			
		||||
        white: '#dbded8',
 | 
			
		||||
        brightBlack: '#686a66',
 | 
			
		||||
        brightRed: '#f54235',
 | 
			
		||||
        brightGreen: '#99e343',
 | 
			
		||||
        brightYellow: '#fdeb61',
 | 
			
		||||
        brightBlue: '#84b0d8',
 | 
			
		||||
        brightMagenta: '#bc94b7',
 | 
			
		||||
        brightCyan: '#37e6e8',
 | 
			
		||||
        brightWhite: '#f1f1f0',
 | 
			
		||||
    } as ITheme,
 | 
			
		||||
    allowProposedApi: true,
 | 
			
		||||
} as ITerminalOptions;
 | 
			
		||||
const flowControl = {
 | 
			
		||||
    limit: 100000,
 | 
			
		||||
    highWater: 10,
 | 
			
		||||
    lowWater: 4,
 | 
			
		||||
} as FlowControl;
 | 
			
		||||
 | 
			
		||||
export class App extends Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <Terminal
 | 
			
		||||
                id="terminal-container"
 | 
			
		||||
                wsUrl={wsUrl}
 | 
			
		||||
                tokenUrl={tokenUrl}
 | 
			
		||||
                clientOptions={clientOptions}
 | 
			
		||||
                termOptions={termOptions}
 | 
			
		||||
                flowControl={flowControl}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								html/src/components/modal/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								html/src/components/modal/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import { h, Component, ComponentChildren } from 'preact';
 | 
			
		||||
 | 
			
		||||
import './modal.scss';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
    show: boolean;
 | 
			
		||||
    children: ComponentChildren;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Modal extends Component<Props> {
 | 
			
		||||
    constructor(props: Props) {
 | 
			
		||||
        super(props);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render({ show, children }: Props) {
 | 
			
		||||
        return (
 | 
			
		||||
            show && (
 | 
			
		||||
                <div className="modal">
 | 
			
		||||
                    <div className="modal-background" />
 | 
			
		||||
                    <div className="modal-content">
 | 
			
		||||
                        <div className="box">{children}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								html/src/components/modal/modal.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								html/src/components/modal/modal.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
.modal {
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  z-index: 40;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-background {
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  background-color: #4a4a4acc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-content {
 | 
			
		||||
  margin: 0 20px;
 | 
			
		||||
  max-height: calc(100vh - 160px);
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
 | 
			
		||||
  .box {
 | 
			
		||||
    background-color: #fff;
 | 
			
		||||
    color: #4a4a4a;
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: 1.25rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  header {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    border-bottom: 1px solid #ddd;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .file-input {
 | 
			
		||||
    height: .01em;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    width: .01em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .file-cta {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    background-color: #f5f5f5;
 | 
			
		||||
    color: #6200ee;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    height: 2.25em;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
    line-height: 1.5;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
    border-color: #dbdbdb;
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    font-size: 1em;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    padding: calc(.375em - 1px) 1em;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media print, screen and (min-width: 769px) {
 | 
			
		||||
  .modal-content {
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    max-height: calc(100vh - 40px);
 | 
			
		||||
    width: 640px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										152
									
								
								html/src/components/terminal/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								html/src/components/terminal/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
			
		||||
import { bind } from 'decko';
 | 
			
		||||
import { Component, h } from 'preact';
 | 
			
		||||
import { Xterm, XtermOptions } from './xterm';
 | 
			
		||||
 | 
			
		||||
import '@xterm/xterm/css/xterm.css';
 | 
			
		||||
import { Modal } from '../modal';
 | 
			
		||||
 | 
			
		||||
interface Props extends XtermOptions {
 | 
			
		||||
    id: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface State {
 | 
			
		||||
    modal: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Terminal extends Component<Props, State> {
 | 
			
		||||
    private container: HTMLElement;
 | 
			
		||||
    private xterm: Xterm;
 | 
			
		||||
    private intervalID: NodeJS.Timeout;
 | 
			
		||||
    private currentDevcontainer = {
 | 
			
		||||
        title: 'Devcontainer Info',
 | 
			
		||||
        detail: 'No Devcontainer Created yet',
 | 
			
		||||
        port: '',
 | 
			
		||||
        ip:'',
 | 
			
		||||
        steps: [
 | 
			
		||||
          // {
 | 
			
		||||
          //   summary: '',
 | 
			
		||||
          //   duration: '',
 | 
			
		||||
          //   status: '',
 | 
			
		||||
          //   logs:{
 | 
			
		||||
          //   },
 | 
			
		||||
          // }
 | 
			
		||||
        ],
 | 
			
		||||
      };
 | 
			
		||||
    constructor(props: Props) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.xterm = new Xterm(props, this.showModal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async componentDidMount() {
 | 
			
		||||
        await this.xterm.refreshToken();
 | 
			
		||||
        const options = new URLSearchParams(decodeURIComponent(window.location.search));
 | 
			
		||||
        
 | 
			
		||||
        const params = new URLSearchParams({
 | 
			
		||||
            repo: options.get('repoid') as string,
 | 
			
		||||
            user: options.get('userid') as string,
 | 
			
		||||
        });
 | 
			
		||||
        fetch('http://' + 
 | 
			
		||||
            options.get('domain') + 
 | 
			
		||||
            ':'+ 
 | 
			
		||||
            options.get('port') +
 | 
			
		||||
            '/' + 
 | 
			
		||||
            options.get('user') + 
 | 
			
		||||
            '/' + 
 | 
			
		||||
            options.get('repo') + 
 | 
			
		||||
            '/devcontainer/status?' + 
 | 
			
		||||
            params
 | 
			
		||||
    )
 | 
			
		||||
            .then(response => response.json())
 | 
			
		||||
            .then(data => {
 | 
			
		||||
                if (data.status !== '-1') {
 | 
			
		||||
                    if (options.get('type') === 'docker') {
 | 
			
		||||
                        this.xterm.open(this.container);
 | 
			
		||||
                        this.xterm.connect();
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.intervalID = setInterval(this.loadOutput, 3000);
 | 
			
		||||
                        this.xterm.open(this.container);
 | 
			
		||||
                        this.xterm.changeUrl(this.currentDevcontainer.ip, this.currentDevcontainer.port)
 | 
			
		||||
                        this.xterm.changeStatus(true);
 | 
			
		||||
                        this.xterm.connect();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch(error => {
 | 
			
		||||
                console.error('Error:', error);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        this.xterm.dispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render({ id }: Props, { modal }: State) {
 | 
			
		||||
        return (
 | 
			
		||||
            <div id={id} ref={c => (this.container = c as HTMLElement)}>
 | 
			
		||||
                <Modal show={modal}>
 | 
			
		||||
                    <label class="file-label">
 | 
			
		||||
                        <input onChange={this.sendFile} class="file-input" type="file" multiple />
 | 
			
		||||
                        <span class="file-cta">Choose files…</span>
 | 
			
		||||
                    </label>
 | 
			
		||||
                </Modal>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    showModal() {
 | 
			
		||||
        this.setState({ modal: true });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    sendFile(event: Event) {
 | 
			
		||||
        this.setState({ modal: false });
 | 
			
		||||
        const files = (event.target as HTMLInputElement).files;
 | 
			
		||||
        if (files) this.xterm.sendFile(files);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private loadOutput() {
 | 
			
		||||
        const options = new URLSearchParams(decodeURIComponent(window.location.search));
 | 
			
		||||
        const params = new URLSearchParams({
 | 
			
		||||
            repo: options.get('repoid') as string,
 | 
			
		||||
            user: options.get('userid') as string,
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        fetch(
 | 
			
		||||
            'http://' + options.get('domain') + ':'+ options.get('port') +'/' +
 | 
			
		||||
                options.get('user') +
 | 
			
		||||
                '/' +
 | 
			
		||||
                options.get('repo') +
 | 
			
		||||
                '/devcontainer/output?' +
 | 
			
		||||
                params
 | 
			
		||||
        )
 | 
			
		||||
            .then(response => response.json())
 | 
			
		||||
            .then(job => {
 | 
			
		||||
                if (!job) {
 | 
			
		||||
                    clearInterval(this.intervalID);
 | 
			
		||||
                    this.intervalID = null as any;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if(this.currentDevcontainer.steps.length < job.currentDevcontainer.steps.length){
 | 
			
		||||
                    for(let i = this.currentDevcontainer.steps.length; i < job.currentDevcontainer.steps.length; i++) {
 | 
			
		||||
                        this.xterm.writeData(job.currentDevcontainer.steps[i].summary);
 | 
			
		||||
                        this.xterm.writeData('\r\n');
 | 
			
		||||
                        for(let j = 0; j < job.currentDevcontainer.steps[i].logs.length; j++) {
 | 
			
		||||
                            this.xterm.writeData(job.currentDevcontainer.steps[i].logs[j].message.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
 | 
			
		||||
                            this.xterm.writeData('\r\n');
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                this.currentDevcontainer = job.currentDevcontainer;
 | 
			
		||||
                if (this.currentDevcontainer.detail === '4' && this.intervalID) {
 | 
			
		||||
                    clearInterval(this.intervalID);
 | 
			
		||||
                    this.intervalID = null as any;
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch(error => {
 | 
			
		||||
                console.error('Error:', error);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								html/src/components/terminal/xterm/addons/overlay.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								html/src/components/terminal/xterm/addons/overlay.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
// ported from hterm.Terminal.prototype.showOverlay
 | 
			
		||||
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
 | 
			
		||||
import { bind } from 'decko';
 | 
			
		||||
import { ITerminalAddon, Terminal } from '@xterm/xterm';
 | 
			
		||||
 | 
			
		||||
export class OverlayAddon implements ITerminalAddon {
 | 
			
		||||
    private terminal: Terminal;
 | 
			
		||||
    private overlayNode: HTMLElement;
 | 
			
		||||
    private overlayTimeout?: number;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.overlayNode = document.createElement('div');
 | 
			
		||||
        this.overlayNode.style.cssText = `border-radius: 15px;
 | 
			
		||||
font-size: xx-large;
 | 
			
		||||
opacity: 0.75;
 | 
			
		||||
padding: 0.2em 0.5em 0.2em 0.5em;
 | 
			
		||||
position: absolute;
 | 
			
		||||
-webkit-user-select: none;
 | 
			
		||||
-webkit-transition: opacity 180ms ease-in;
 | 
			
		||||
-moz-user-select: none;
 | 
			
		||||
-moz-transition: opacity 180ms ease-in;`;
 | 
			
		||||
 | 
			
		||||
        this.overlayNode.addEventListener(
 | 
			
		||||
            'mousedown',
 | 
			
		||||
            e => {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
            },
 | 
			
		||||
            true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    activate(terminal: Terminal): void {
 | 
			
		||||
        this.terminal = terminal;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dispose(): void {}
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    showOverlay(msg: string, timeout?: number): void {
 | 
			
		||||
        const { terminal, overlayNode } = this;
 | 
			
		||||
        if (!terminal.element) return;
 | 
			
		||||
 | 
			
		||||
        overlayNode.style.color = '#101010';
 | 
			
		||||
        overlayNode.style.backgroundColor = '#f0f0f0';
 | 
			
		||||
        overlayNode.textContent = msg;
 | 
			
		||||
        overlayNode.style.opacity = '0.75';
 | 
			
		||||
 | 
			
		||||
        if (!overlayNode.parentNode) {
 | 
			
		||||
            terminal.element.appendChild(overlayNode);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const divSize = terminal.element.getBoundingClientRect();
 | 
			
		||||
        const overlaySize = overlayNode.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
 | 
			
		||||
        overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
 | 
			
		||||
 | 
			
		||||
        if (this.overlayTimeout) clearTimeout(this.overlayTimeout);
 | 
			
		||||
        if (!timeout) return;
 | 
			
		||||
 | 
			
		||||
        this.overlayTimeout = window.setTimeout(() => {
 | 
			
		||||
            overlayNode.style.opacity = '0';
 | 
			
		||||
            this.overlayTimeout = window.setTimeout(() => {
 | 
			
		||||
                if (overlayNode.parentNode) {
 | 
			
		||||
                    overlayNode.parentNode.removeChild(overlayNode);
 | 
			
		||||
                }
 | 
			
		||||
                this.overlayTimeout = undefined;
 | 
			
		||||
                overlayNode.style.opacity = '0.75';
 | 
			
		||||
            }, 200);
 | 
			
		||||
        }, timeout || 1500);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										182
									
								
								html/src/components/terminal/xterm/addons/zmodem.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								html/src/components/terminal/xterm/addons/zmodem.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,182 @@
 | 
			
		||||
import { bind } from 'decko';
 | 
			
		||||
import { saveAs } from 'file-saver';
 | 
			
		||||
import { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
 | 
			
		||||
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
 | 
			
		||||
import { TrzszFilter } from 'trzsz';
 | 
			
		||||
 | 
			
		||||
export interface ZmodeOptions {
 | 
			
		||||
    zmodem: boolean;
 | 
			
		||||
    trzsz: boolean;
 | 
			
		||||
    windows: boolean;
 | 
			
		||||
    trzszDragInitTimeout: number;
 | 
			
		||||
    onSend: () => void;
 | 
			
		||||
    sender: (data: string | Uint8Array) => void;
 | 
			
		||||
    writer: (data: string | Uint8Array) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ZmodemAddon implements ITerminalAddon {
 | 
			
		||||
    private disposables: IDisposable[] = [];
 | 
			
		||||
    private terminal: Terminal;
 | 
			
		||||
    private sentry: Zmodem.Sentry;
 | 
			
		||||
    private session: Zmodem.Session;
 | 
			
		||||
    private denier: () => void;
 | 
			
		||||
    private trzszFilter: TrzszFilter;
 | 
			
		||||
 | 
			
		||||
    constructor(private options: ZmodeOptions) {}
 | 
			
		||||
 | 
			
		||||
    activate(terminal: Terminal) {
 | 
			
		||||
        this.terminal = terminal;
 | 
			
		||||
        if (this.options.zmodem) this.zmodemInit();
 | 
			
		||||
        if (this.options.trzsz) this.trzszInit();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dispose() {
 | 
			
		||||
        for (const d of this.disposables) {
 | 
			
		||||
            d.dispose();
 | 
			
		||||
        }
 | 
			
		||||
        this.disposables.length = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    consume(data: ArrayBuffer) {
 | 
			
		||||
        try {
 | 
			
		||||
            if (this.options.trzsz) {
 | 
			
		||||
                this.trzszFilter.processServerOutput(data);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.sentry.consume(data);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error('[ttyd] zmodem consume: ', e);
 | 
			
		||||
            this.reset();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private reset() {
 | 
			
		||||
        this.terminal.options.disableStdin = false;
 | 
			
		||||
        this.terminal.focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private addDisposableListener(target: EventTarget, type: string, listener: EventListener) {
 | 
			
		||||
        target.addEventListener(type, listener);
 | 
			
		||||
        this.disposables.push({ dispose: () => target.removeEventListener(type, listener) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private trzszInit() {
 | 
			
		||||
        const { terminal } = this;
 | 
			
		||||
        const { sender, writer, zmodem } = this.options;
 | 
			
		||||
        this.trzszFilter = new TrzszFilter({
 | 
			
		||||
            writeToTerminal: data => {
 | 
			
		||||
                if (!this.trzszFilter.isTransferringFiles() && zmodem) {
 | 
			
		||||
                    this.sentry.consume(data);
 | 
			
		||||
                } else {
 | 
			
		||||
                    writer(typeof data === 'string' ? data : new Uint8Array(data as ArrayBuffer));
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            sendToServer: data => sender(data),
 | 
			
		||||
            terminalColumns: terminal.cols,
 | 
			
		||||
            isWindowsShell: this.options.windows,
 | 
			
		||||
            dragInitTimeout: this.options.trzszDragInitTimeout,
 | 
			
		||||
        });
 | 
			
		||||
        const element = terminal.element as EventTarget;
 | 
			
		||||
        this.addDisposableListener(element, 'dragover', event => event.preventDefault());
 | 
			
		||||
        this.addDisposableListener(element, 'drop', event => {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
            this.trzszFilter
 | 
			
		||||
                .uploadFiles((event as DragEvent).dataTransfer?.items as DataTransferItemList)
 | 
			
		||||
                .then(() => console.log('[ttyd] upload success'))
 | 
			
		||||
                .catch(err => console.log('[ttyd] upload failed: ' + err));
 | 
			
		||||
        });
 | 
			
		||||
        this.disposables.push(terminal.onResize(size => this.trzszFilter.setTerminalColumns(size.cols)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private zmodemInit() {
 | 
			
		||||
        const { sender, writer } = this.options;
 | 
			
		||||
        const { terminal, reset, zmodemDetect } = this;
 | 
			
		||||
        this.session = null;
 | 
			
		||||
        this.sentry = new Zmodem.Sentry({
 | 
			
		||||
            to_terminal: octets => writer(new Uint8Array(octets)),
 | 
			
		||||
            sender: octets => sender(new Uint8Array(octets)),
 | 
			
		||||
            on_retract: () => reset(),
 | 
			
		||||
            on_detect: detection => zmodemDetect(detection),
 | 
			
		||||
        });
 | 
			
		||||
        this.disposables.push(
 | 
			
		||||
            terminal.onKey(e => {
 | 
			
		||||
                const event = e.domEvent;
 | 
			
		||||
                if (event.ctrlKey && event.key === 'c') {
 | 
			
		||||
                    if (this.denier) this.denier();
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private zmodemDetect(detection: Zmodem.Detection): void {
 | 
			
		||||
        const { terminal, receiveFile } = this;
 | 
			
		||||
        terminal.options.disableStdin = true;
 | 
			
		||||
 | 
			
		||||
        this.denier = () => detection.deny();
 | 
			
		||||
        this.session = detection.confirm();
 | 
			
		||||
        this.session.on('session_end', () => this.reset());
 | 
			
		||||
 | 
			
		||||
        if (this.session.type === 'send') {
 | 
			
		||||
            this.options.onSend();
 | 
			
		||||
        } else {
 | 
			
		||||
            receiveFile();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public sendFile(files: FileList) {
 | 
			
		||||
        const { session, writeProgress } = this;
 | 
			
		||||
        Zmodem.Browser.send_files(session, files, {
 | 
			
		||||
            on_progress: (_, offer) => writeProgress(offer),
 | 
			
		||||
        })
 | 
			
		||||
            .then(() => session.close())
 | 
			
		||||
            .catch(() => this.reset());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private receiveFile() {
 | 
			
		||||
        const { session, writeProgress } = this;
 | 
			
		||||
 | 
			
		||||
        session.on('offer', offer => {
 | 
			
		||||
            offer.on('input', () => writeProgress(offer));
 | 
			
		||||
            offer
 | 
			
		||||
                .accept()
 | 
			
		||||
                .then(payloads => {
 | 
			
		||||
                    const blob = new Blob(payloads, { type: 'application/octet-stream' });
 | 
			
		||||
                    saveAs(blob, offer.get_details().name);
 | 
			
		||||
                })
 | 
			
		||||
                .catch(() => this.reset());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        session.start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private writeProgress(offer: Zmodem.Offer) {
 | 
			
		||||
        const { bytesHuman } = this;
 | 
			
		||||
        const file = offer.get_details();
 | 
			
		||||
        const name = file.name;
 | 
			
		||||
        const size = file.size;
 | 
			
		||||
        const offset = offer.get_offset();
 | 
			
		||||
        const percent = ((100 * offset) / size).toFixed(2);
 | 
			
		||||
 | 
			
		||||
        this.options.writer(`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    private bytesHuman(bytes: any, precision: number): string {
 | 
			
		||||
        if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
 | 
			
		||||
            return '-';
 | 
			
		||||
        }
 | 
			
		||||
        if (bytes === 0) return '0';
 | 
			
		||||
        if (typeof precision === 'undefined') precision = 1;
 | 
			
		||||
        const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
 | 
			
		||||
        const num = Math.floor(Math.log(bytes) / Math.log(1024));
 | 
			
		||||
        const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
 | 
			
		||||
        return `${value} ${units[num]}`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										652
									
								
								html/src/components/terminal/xterm/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										652
									
								
								html/src/components/terminal/xterm/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,652 @@
 | 
			
		||||
import { bind } from 'decko';
 | 
			
		||||
import type { IDisposable, ITerminalOptions } from '@xterm/xterm';
 | 
			
		||||
import { Terminal } from '@xterm/xterm';
 | 
			
		||||
import { CanvasAddon } from '@xterm/addon-canvas';
 | 
			
		||||
import { ClipboardAddon } from '@xterm/addon-clipboard';
 | 
			
		||||
import { WebglAddon } from '@xterm/addon-webgl';
 | 
			
		||||
import { FitAddon } from '@xterm/addon-fit';
 | 
			
		||||
import { WebLinksAddon } from '@xterm/addon-web-links';
 | 
			
		||||
import { ImageAddon } from '@xterm/addon-image';
 | 
			
		||||
import { Unicode11Addon } from '@xterm/addon-unicode11';
 | 
			
		||||
import { OverlayAddon } from './addons/overlay';
 | 
			
		||||
import { ZmodemAddon } from './addons/zmodem';
 | 
			
		||||
 | 
			
		||||
import '@xterm/xterm/css/xterm.css';
 | 
			
		||||
 | 
			
		||||
interface TtydTerminal extends Terminal {
 | 
			
		||||
    fit(): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface Window {
 | 
			
		||||
        term: TtydTerminal;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Command {
 | 
			
		||||
    // server side
 | 
			
		||||
    OUTPUT = '0',
 | 
			
		||||
    SET_WINDOW_TITLE = '1',
 | 
			
		||||
    SET_PREFERENCES = '2',
 | 
			
		||||
 | 
			
		||||
    // client side
 | 
			
		||||
    INPUT = '0',
 | 
			
		||||
    RESIZE_TERMINAL = '1',
 | 
			
		||||
    PAUSE = '2',
 | 
			
		||||
    RESUME = '3',
 | 
			
		||||
}
 | 
			
		||||
type Preferences = ITerminalOptions & ClientOptions;
 | 
			
		||||
 | 
			
		||||
export type RendererType = 'dom' | 'canvas' | 'webgl';
 | 
			
		||||
 | 
			
		||||
export interface ClientOptions {
 | 
			
		||||
    rendererType: RendererType;
 | 
			
		||||
    disableLeaveAlert: boolean;
 | 
			
		||||
    disableResizeOverlay: boolean;
 | 
			
		||||
    enableZmodem: boolean;
 | 
			
		||||
    enableTrzsz: boolean;
 | 
			
		||||
    enableSixel: boolean;
 | 
			
		||||
    titleFixed?: string;
 | 
			
		||||
    isWindows: boolean;
 | 
			
		||||
    trzszDragInitTimeout: number;
 | 
			
		||||
    unicodeVersion: string;
 | 
			
		||||
    closeOnDisconnect: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FlowControl {
 | 
			
		||||
    limit: number;
 | 
			
		||||
    highWater: number;
 | 
			
		||||
    lowWater: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface XtermOptions {
 | 
			
		||||
    wsUrl: string;
 | 
			
		||||
    tokenUrl: string;
 | 
			
		||||
    flowControl: FlowControl;
 | 
			
		||||
    clientOptions: ClientOptions;
 | 
			
		||||
    termOptions: ITerminalOptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toDisposable(f: () => void): IDisposable {
 | 
			
		||||
    return { dispose: f };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addEventListener(target: EventTarget, type: string, listener: EventListener): IDisposable {
 | 
			
		||||
    target.addEventListener(type, listener);
 | 
			
		||||
    return toDisposable(() => target.removeEventListener(type, listener));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Xterm {
 | 
			
		||||
    private disposables: IDisposable[] = [];
 | 
			
		||||
    private textEncoder = new TextEncoder();
 | 
			
		||||
    private textDecoder = new TextDecoder();
 | 
			
		||||
    private written = 0;
 | 
			
		||||
    private pending = 0;
 | 
			
		||||
 | 
			
		||||
    private terminal: Terminal;
 | 
			
		||||
    private fitAddon = new FitAddon();
 | 
			
		||||
    private overlayAddon = new OverlayAddon();
 | 
			
		||||
    private clipboardAddon = new ClipboardAddon();
 | 
			
		||||
    private webLinksAddon = new WebLinksAddon();
 | 
			
		||||
    private webglAddon?: WebglAddon;
 | 
			
		||||
    private canvasAddon?: CanvasAddon;
 | 
			
		||||
    private zmodemAddon?: ZmodemAddon;
 | 
			
		||||
 | 
			
		||||
    private socket?: WebSocket;
 | 
			
		||||
    private token: string;
 | 
			
		||||
    private opened = false;
 | 
			
		||||
    private title?: string;
 | 
			
		||||
    private titleFixed?: string;
 | 
			
		||||
    private resizeOverlay = true;
 | 
			
		||||
    private reconnect = true;
 | 
			
		||||
    private doReconnect = true;
 | 
			
		||||
    private closeOnDisconnect = false;
 | 
			
		||||
    private intervalID: NodeJS.Timeout;
 | 
			
		||||
    private writeFunc = (data: ArrayBuffer) => this.writeData(new Uint8Array(data));
 | 
			
		||||
    private status = false;
 | 
			
		||||
    private titleStatus = false;
 | 
			
		||||
    private checkStatus = false;
 | 
			
		||||
    private connectStatus = false;
 | 
			
		||||
    private beforeCommand?: string;
 | 
			
		||||
    constructor(
 | 
			
		||||
        private options: XtermOptions,
 | 
			
		||||
        private sendCb: () => void
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    dispose() {
 | 
			
		||||
        for (const d of this.disposables) {
 | 
			
		||||
            d.dispose();
 | 
			
		||||
        }
 | 
			
		||||
        this.disposables.length = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private register<T extends IDisposable>(d: T): T {
 | 
			
		||||
        this.disposables.push(d);
 | 
			
		||||
        return d;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public sendFile(files: FileList) {
 | 
			
		||||
        this.zmodemAddon?.sendFile(files);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public async refreshToken() {
 | 
			
		||||
        try {
 | 
			
		||||
            const resp = await fetch(this.options.tokenUrl);
 | 
			
		||||
            if (resp.ok) {
 | 
			
		||||
                const json = await resp.json();
 | 
			
		||||
                this.token = json.token;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(`[ttyd] fetch ${this.options.tokenUrl}: `, e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private onWindowUnload(event: BeforeUnloadEvent) {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        if (this.socket?.readyState === WebSocket.OPEN) {
 | 
			
		||||
            const message = 'Close terminal? this will also terminate the command.';
 | 
			
		||||
            event.returnValue = message;
 | 
			
		||||
            return message;
 | 
			
		||||
        }
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public open(parent: HTMLElement) {
 | 
			
		||||
        this.terminal = new Terminal(this.options.termOptions);
 | 
			
		||||
        const { terminal, fitAddon, overlayAddon, clipboardAddon, webLinksAddon } = this;
 | 
			
		||||
        window.term = terminal as TtydTerminal;
 | 
			
		||||
        window.term.fit = () => {
 | 
			
		||||
            this.fitAddon.fit();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        terminal.loadAddon(fitAddon);
 | 
			
		||||
        terminal.loadAddon(overlayAddon);
 | 
			
		||||
        terminal.loadAddon(clipboardAddon);
 | 
			
		||||
        terminal.loadAddon(webLinksAddon);
 | 
			
		||||
 | 
			
		||||
        terminal.open(parent);
 | 
			
		||||
        fitAddon.fit();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private initListeners() {
 | 
			
		||||
        const { terminal, fitAddon, overlayAddon, register, sendData } = this;
 | 
			
		||||
        register(
 | 
			
		||||
            terminal.onTitleChange(data => {
 | 
			
		||||
                if (data && data !== '' && !this.titleFixed) {
 | 
			
		||||
                    document.title = data + ' | ' + this.title;
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
        register(
 | 
			
		||||
            terminal.onData(data =>
 | 
			
		||||
            {
 | 
			
		||||
                if (this.status) {
 | 
			
		||||
                    sendData(data);
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.writeData('\b \b');
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
        register(terminal.onBinary(data => sendData(Uint8Array.from(data, v => v.charCodeAt(0)))));
 | 
			
		||||
        register(
 | 
			
		||||
            terminal.onResize(({ cols, rows }) => {
 | 
			
		||||
                const msg = JSON.stringify({ columns: cols, rows: rows });
 | 
			
		||||
                this.socket?.send(this.textEncoder.encode(Command.RESIZE_TERMINAL + msg));
 | 
			
		||||
                if (this.resizeOverlay) overlayAddon.showOverlay(`${cols}x${rows}`, 300);
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
        register(
 | 
			
		||||
            terminal.onSelectionChange(() => {
 | 
			
		||||
                if (this.terminal.getSelection() === '') return;
 | 
			
		||||
                try {
 | 
			
		||||
                    document.execCommand('copy');
 | 
			
		||||
                } catch (e) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                this.overlayAddon?.showOverlay('\u2702', 200);
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
        register(addEventListener(window, 'resize', () => fitAddon.fit()));
 | 
			
		||||
        register(addEventListener(window, 'beforeunload', this.onWindowUnload));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public writeData(data: string | Uint8Array) {
 | 
			
		||||
        const { terminal, textEncoder } = this;
 | 
			
		||||
        const { limit, highWater, lowWater } = this.options.flowControl;
 | 
			
		||||
 | 
			
		||||
        this.written += data.length;
 | 
			
		||||
        if (this.written > limit) {
 | 
			
		||||
            terminal.write(data, () => {
 | 
			
		||||
                this.pending = Math.max(this.pending - 1, 0);
 | 
			
		||||
                if (this.pending < lowWater) {
 | 
			
		||||
                    this.socket?.send(textEncoder.encode(Command.RESUME));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            this.pending++;
 | 
			
		||||
            this.written = 0;
 | 
			
		||||
            if (this.pending > highWater) {
 | 
			
		||||
                this.socket?.send(textEncoder.encode(Command.PAUSE));
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            terminal.write(data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public sendData(data: string | Uint8Array) {
 | 
			
		||||
        const { socket, textEncoder } = this;
 | 
			
		||||
        if (socket?.readyState !== WebSocket.OPEN) return;
 | 
			
		||||
 | 
			
		||||
        if (typeof data === 'string') {
 | 
			
		||||
            const payload = new Uint8Array(data.length * 3 + 1);
 | 
			
		||||
            payload[0] = Command.INPUT.charCodeAt(0);
 | 
			
		||||
            const stats = textEncoder.encodeInto(data, payload.subarray(1));
 | 
			
		||||
            socket.send(payload.subarray(0, (stats.written as number) + 1));
 | 
			
		||||
        } else {
 | 
			
		||||
            const payload = new Uint8Array(data.length + 1);
 | 
			
		||||
            payload[0] = Command.INPUT.charCodeAt(0);
 | 
			
		||||
            payload.set(data, 1);
 | 
			
		||||
            socket.send(payload);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public changeUrl(ip: string, port: string) {
 | 
			
		||||
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
 | 
			
		||||
        this.options.wsUrl = [protocol, '//' + ip + ':' + port +'/ws', window.location.search].join('');
 | 
			
		||||
        this.options.tokenUrl = [window.location.protocol, '//' + ip + ':' + port +'/token'].join('');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public changeStatus(v: boolean){
 | 
			
		||||
        this.status = v;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    public connect() {
 | 
			
		||||
        this.socket = new WebSocket(this.options.wsUrl, ['tty']);
 | 
			
		||||
        const { socket, register } = this;
 | 
			
		||||
 | 
			
		||||
        socket.binaryType = 'arraybuffer';
 | 
			
		||||
        register(addEventListener(socket, 'open', this.onSocketOpen));
 | 
			
		||||
        register(addEventListener(socket, 'message', this.onSocketData as EventListener));
 | 
			
		||||
        register(addEventListener(socket, 'close', this.onSocketClose as EventListener));
 | 
			
		||||
        register(addEventListener(socket, 'error', () => (this.doReconnect = false)));
 | 
			
		||||
        const options = new URLSearchParams(decodeURIComponent(window.location.search));
 | 
			
		||||
        if (options.get('type') === 'docker') {
 | 
			
		||||
            this.intervalID = setInterval(this.loadCommand, 3000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private onSocketOpen() {
 | 
			
		||||
        console.log('[ttyd] websocket connection opened');
 | 
			
		||||
 | 
			
		||||
        const { textEncoder, terminal, overlayAddon } = this;
 | 
			
		||||
        const msg = JSON.stringify({ AuthToken: this.token, columns: terminal.cols, rows: terminal.rows });
 | 
			
		||||
        this.socket?.send(textEncoder.encode(msg));
 | 
			
		||||
 | 
			
		||||
        if (this.opened) {
 | 
			
		||||
            terminal.reset();
 | 
			
		||||
            terminal.options.disableStdin = false;
 | 
			
		||||
            overlayAddon.showOverlay('Reconnected', 300);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.opened = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.doReconnect = this.reconnect;
 | 
			
		||||
        this.initListeners();
 | 
			
		||||
        terminal.focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private onSocketClose(event: CloseEvent) {
 | 
			
		||||
        console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
 | 
			
		||||
 | 
			
		||||
        const { refreshToken, connect, doReconnect, overlayAddon } = this;
 | 
			
		||||
        overlayAddon.showOverlay('Connection Closed');
 | 
			
		||||
        this.dispose();
 | 
			
		||||
 | 
			
		||||
        // 1000: CLOSE_NORMAL
 | 
			
		||||
        if (event.code !== 1000 && doReconnect) {
 | 
			
		||||
            overlayAddon.showOverlay('Reconnecting...');
 | 
			
		||||
            refreshToken().then(connect);
 | 
			
		||||
        } else if (this.closeOnDisconnect) {
 | 
			
		||||
            window.close();
 | 
			
		||||
        } else {
 | 
			
		||||
            const { terminal } = this;
 | 
			
		||||
            const keyDispose = terminal.onKey(e => {
 | 
			
		||||
                const event = e.domEvent;
 | 
			
		||||
                if (event.key === 'Enter') {
 | 
			
		||||
                    keyDispose.dispose();
 | 
			
		||||
                    overlayAddon.showOverlay('Reconnecting...');
 | 
			
		||||
                    refreshToken().then(connect);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            overlayAddon.showOverlay('Press ⏎ to Reconnect');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private loadCommand() {
 | 
			
		||||
        const options = new URLSearchParams(decodeURIComponent(window.location.search));
 | 
			
		||||
        const params = new URLSearchParams({
 | 
			
		||||
            repo: options.get('repoid') as string,
 | 
			
		||||
            user: options.get('userid') as string,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        fetch(
 | 
			
		||||
            'http://' + options.get('domain') + ':'+ options.get('port') +'/' +
 | 
			
		||||
                options.get('user') +
 | 
			
		||||
                '/' +
 | 
			
		||||
                options.get('repo') +
 | 
			
		||||
                '/devcontainer/command?' +
 | 
			
		||||
                params
 | 
			
		||||
        )
 | 
			
		||||
            .then(response => response.json())
 | 
			
		||||
            .then(data => {
 | 
			
		||||
                if (data.status !== '4' && data.status !== '0') {
 | 
			
		||||
                    this.sendData(data.command);
 | 
			
		||||
                } else {
 | 
			
		||||
                    clearInterval(this.intervalID);
 | 
			
		||||
                    if (data.status === '4') {
 | 
			
		||||
                        fetch(
 | 
			
		||||
                            'http://' + options.get('domain') + ':'+ options.get('port') +'/' +
 | 
			
		||||
                                options.get('user') +
 | 
			
		||||
                                '/' +
 | 
			
		||||
                                options.get('repo') +
 | 
			
		||||
                                '/devcontainer/command?' +
 | 
			
		||||
                                params
 | 
			
		||||
                        )
 | 
			
		||||
                            .then(response => response.json())
 | 
			
		||||
                            .then(data => {
 | 
			
		||||
                                this.sendData(data.command);
 | 
			
		||||
                            })
 | 
			
		||||
                            .catch(error => {
 | 
			
		||||
                                console.error('Error:', error);
 | 
			
		||||
                            });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch(error => {
 | 
			
		||||
                console.error('Error:', error);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private parseOptsFromUrlQuery(query: string): Preferences {
 | 
			
		||||
        const { terminal } = this;
 | 
			
		||||
        const { clientOptions } = this.options;
 | 
			
		||||
        const prefs = {} as Preferences;
 | 
			
		||||
        const queryObj = Array.from(new URLSearchParams(query) as unknown as Iterable<[string, string]>);
 | 
			
		||||
 | 
			
		||||
        for (const [k, queryVal] of queryObj) {
 | 
			
		||||
            let v = clientOptions[k];
 | 
			
		||||
            if (v === undefined) v = terminal.options[k];
 | 
			
		||||
            switch (typeof v) {
 | 
			
		||||
                case 'boolean':
 | 
			
		||||
                    prefs[k] = queryVal === 'true' || queryVal === '1';
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'number':
 | 
			
		||||
                case 'bigint':
 | 
			
		||||
                    prefs[k] = Number.parseInt(queryVal, 10);
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'string':
 | 
			
		||||
                    prefs[k] = queryVal;
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'object':
 | 
			
		||||
                    prefs[k] = JSON.parse(queryVal);
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    console.warn(`[ttyd] maybe unknown option: ${k}=${queryVal}, treating as string`);
 | 
			
		||||
                    prefs[k] = queryVal;
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return prefs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private onSocketData(event: MessageEvent) {
 | 
			
		||||
        const { textDecoder } = this;
 | 
			
		||||
        const rawData = event.data as ArrayBuffer;
 | 
			
		||||
        const cmd = String.fromCharCode(new Uint8Array(rawData)[0]);
 | 
			
		||||
        const data = rawData.slice(1);
 | 
			
		||||
        switch (cmd) {
 | 
			
		||||
            case Command.OUTPUT:
 | 
			
		||||
                const options = new URLSearchParams(decodeURIComponent(window.location.search));
 | 
			
		||||
                if (options.get('type') === 'docker') {
 | 
			
		||||
                    if (
 | 
			
		||||
                        this.status === false &&
 | 
			
		||||
                        textDecoder.decode(data).replace(/\s/g, '').includes('Successfully connected to the container'.replace(/\s/g, ''))
 | 
			
		||||
                    ) {
 | 
			
		||||
                        if(this.connectStatus == true){
 | 
			
		||||
                            this.status = true;
 | 
			
		||||
                        }
 | 
			
		||||
                        this.connectStatus = true;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (this.checkStatus) {
 | 
			
		||||
                        if (textDecoder.decode(data).replace(/\s/g, '').includes('You have out the container. Please refresh the terminal to reconnect.'.replace(/\s/g, ''))) {
 | 
			
		||||
                            this.checkStatus = false;
 | 
			
		||||
                            this.status = false;
 | 
			
		||||
                            this.connectStatus = false;
 | 
			
		||||
                        }
 | 
			
		||||
                        if(textDecoder.decode(data).includes('\x1b')){
 | 
			
		||||
                            this.checkStatus = false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (this.titleStatus && textDecoder.decode(data).includes('\x1b')) {
 | 
			
		||||
                        this.titleStatus = false;
 | 
			
		||||
                        this.checkStatus = true;
 | 
			
		||||
                        this.sendData(
 | 
			
		||||
                            `echo $WEB_TERMINAL\n`
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                    if ( 
 | 
			
		||||
                        !(this.status === false && (textDecoder.decode(data).includes('\x1b') || textDecoder.decode(data).replace(/\s/g, '').includes('docker'))) &&
 | 
			
		||||
                        this.titleStatus !== true &&
 | 
			
		||||
                        this.checkStatus !== true
 | 
			
		||||
                    ){
 | 
			
		||||
                        this.writeFunc(data);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (textDecoder.decode(data).replace(/\s/g, '').includes('exit')) {
 | 
			
		||||
                        this.titleStatus = true;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.writeFunc(data);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case Command.SET_WINDOW_TITLE:
 | 
			
		||||
                console.log('SET_WINDOW_TITLESET_WINDOW_TITLE');
 | 
			
		||||
                this.title = textDecoder.decode(data);
 | 
			
		||||
                document.title = this.title;
 | 
			
		||||
                break;
 | 
			
		||||
            case Command.SET_PREFERENCES:
 | 
			
		||||
                console.log('SET_PREFERENCESSET_PREFERENCESSET_PREFERENCES');
 | 
			
		||||
                this.applyPreferences({
 | 
			
		||||
                    ...this.options.clientOptions,
 | 
			
		||||
                    ...JSON.parse(textDecoder.decode(data)),
 | 
			
		||||
                    ...this.parseOptsFromUrlQuery(window.location.search),
 | 
			
		||||
                } as Preferences);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                console.warn(`[ttyd] unknown command: ${cmd}`);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private applyPreferences(prefs: Preferences) {
 | 
			
		||||
        const { terminal, fitAddon, register } = this;
 | 
			
		||||
        if (prefs.enableZmodem || prefs.enableTrzsz) {
 | 
			
		||||
            this.zmodemAddon = new ZmodemAddon({
 | 
			
		||||
                zmodem: prefs.enableZmodem,
 | 
			
		||||
                trzsz: prefs.enableTrzsz,
 | 
			
		||||
                windows: prefs.isWindows,
 | 
			
		||||
                trzszDragInitTimeout: prefs.trzszDragInitTimeout,
 | 
			
		||||
                onSend: this.sendCb,
 | 
			
		||||
                sender: this.sendData,
 | 
			
		||||
                writer: this.writeData,
 | 
			
		||||
            });
 | 
			
		||||
            this.writeFunc = data => this.zmodemAddon?.consume(data);
 | 
			
		||||
            terminal.loadAddon(register(this.zmodemAddon));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const [key, value] of Object.entries(prefs)) {
 | 
			
		||||
            switch (key) {
 | 
			
		||||
                case 'rendererType':
 | 
			
		||||
                    this.setRendererType(value);
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'disableLeaveAlert':
 | 
			
		||||
                    if (value) {
 | 
			
		||||
                        window.removeEventListener('beforeunload', this.onWindowUnload);
 | 
			
		||||
                        console.log('[ttyd] Leave site alert disabled');
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'disableResizeOverlay':
 | 
			
		||||
                    if (value) {
 | 
			
		||||
                        console.log('[ttyd] Resize overlay disabled');
 | 
			
		||||
                        this.resizeOverlay = false;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'disableReconnect':
 | 
			
		||||
                    if (value) {
 | 
			
		||||
                        console.log('[ttyd] Reconnect disabled');
 | 
			
		||||
                        this.reconnect = false;
 | 
			
		||||
                        this.doReconnect = false;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'enableZmodem':
 | 
			
		||||
                    if (value) console.log('[ttyd] Zmodem enabled');
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'enableTrzsz':
 | 
			
		||||
                    if (value) console.log('[ttyd] trzsz enabled');
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'trzszDragInitTimeout':
 | 
			
		||||
                    if (value) console.log(`[ttyd] trzsz drag init timeout: ${value}`);
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'enableSixel':
 | 
			
		||||
                    if (value) {
 | 
			
		||||
                        terminal.loadAddon(register(new ImageAddon()));
 | 
			
		||||
                        console.log('[ttyd] Sixel enabled');
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'closeOnDisconnect':
 | 
			
		||||
                    if (value) {
 | 
			
		||||
                        console.log('[ttyd] close on disconnect enabled (Reconnect disabled)');
 | 
			
		||||
                        this.closeOnDisconnect = true;
 | 
			
		||||
                        this.reconnect = false;
 | 
			
		||||
                        this.doReconnect = false;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'titleFixed':
 | 
			
		||||
                    if (!value || value === '') return;
 | 
			
		||||
                    console.log(`[ttyd] setting fixed title: ${value}`);
 | 
			
		||||
                    this.titleFixed = value;
 | 
			
		||||
                    document.title = value;
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'isWindows':
 | 
			
		||||
                    if (value) console.log('[ttyd] is windows');
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'unicodeVersion':
 | 
			
		||||
                    switch (value) {
 | 
			
		||||
                        case 6:
 | 
			
		||||
                        case '6':
 | 
			
		||||
                            console.log('[ttyd] setting Unicode version: 6');
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 11:
 | 
			
		||||
                        case '11':
 | 
			
		||||
                        default:
 | 
			
		||||
                            console.log('[ttyd] setting Unicode version: 11');
 | 
			
		||||
                            terminal.loadAddon(new Unicode11Addon());
 | 
			
		||||
                            terminal.unicode.activeVersion = '11';
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    console.log(`[ttyd] option: ${key}=${JSON.stringify(value)}`);
 | 
			
		||||
                    if (terminal.options[key] instanceof Object) {
 | 
			
		||||
                        terminal.options[key] = Object.assign({}, terminal.options[key], value);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        terminal.options[key] = value;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (key.indexOf('font') === 0) fitAddon.fit();
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @bind
 | 
			
		||||
    private setRendererType(value: RendererType) {
 | 
			
		||||
        const { terminal } = this;
 | 
			
		||||
        const disposeCanvasRenderer = () => {
 | 
			
		||||
            try {
 | 
			
		||||
                this.canvasAddon?.dispose();
 | 
			
		||||
            } catch {
 | 
			
		||||
                // ignore
 | 
			
		||||
            }
 | 
			
		||||
            this.canvasAddon = undefined;
 | 
			
		||||
        };
 | 
			
		||||
        const disposeWebglRenderer = () => {
 | 
			
		||||
            try {
 | 
			
		||||
                this.webglAddon?.dispose();
 | 
			
		||||
            } catch {
 | 
			
		||||
                // ignore
 | 
			
		||||
            }
 | 
			
		||||
            this.webglAddon = undefined;
 | 
			
		||||
        };
 | 
			
		||||
        const enableCanvasRenderer = () => {
 | 
			
		||||
            if (this.canvasAddon) return;
 | 
			
		||||
            this.canvasAddon = new CanvasAddon();
 | 
			
		||||
            disposeWebglRenderer();
 | 
			
		||||
            try {
 | 
			
		||||
                this.terminal.loadAddon(this.canvasAddon);
 | 
			
		||||
                console.log('[ttyd] canvas renderer loaded');
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.log('[ttyd] canvas renderer could not be loaded, falling back to dom renderer', e);
 | 
			
		||||
                disposeCanvasRenderer();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        const enableWebglRenderer = () => {
 | 
			
		||||
            if (this.webglAddon) return;
 | 
			
		||||
            this.webglAddon = new WebglAddon();
 | 
			
		||||
            disposeCanvasRenderer();
 | 
			
		||||
            try {
 | 
			
		||||
                this.webglAddon.onContextLoss(() => {
 | 
			
		||||
                    this.webglAddon?.dispose();
 | 
			
		||||
                });
 | 
			
		||||
                terminal.loadAddon(this.webglAddon);
 | 
			
		||||
                console.log('[ttyd] WebGL renderer loaded');
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.log('[ttyd] WebGL renderer could not be loaded, falling back to canvas renderer', e);
 | 
			
		||||
                disposeWebglRenderer();
 | 
			
		||||
                enableCanvasRenderer();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        switch (value) {
 | 
			
		||||
            case 'canvas':
 | 
			
		||||
                enableCanvasRenderer();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'webgl':
 | 
			
		||||
                enableWebglRenderer();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'dom':
 | 
			
		||||
                disposeWebglRenderer();
 | 
			
		||||
                disposeCanvasRenderer();
 | 
			
		||||
                console.log('[ttyd] dom renderer loaded');
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								html/src/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								html/src/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										9
									
								
								html/src/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								html/src/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
if (process.env.NODE_ENV === 'development') {
 | 
			
		||||
    require('preact/debug');
 | 
			
		||||
}
 | 
			
		||||
import 'whatwg-fetch';
 | 
			
		||||
import { h, render } from 'preact';
 | 
			
		||||
import { App } from './components/app';
 | 
			
		||||
import './style/index.scss';
 | 
			
		||||
 | 
			
		||||
render(<App />, document.body);
 | 
			
		||||
							
								
								
									
										18
									
								
								html/src/style/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								html/src/style/index.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
html,
 | 
			
		||||
body {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  min-height: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#terminal-container {
 | 
			
		||||
  width: auto;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  .terminal {
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    height: calc(100% - 10px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								html/src/template.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								html/src/template.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 | 
			
		||||
    <meta name="mobile-web-app-capable" content="yes">
 | 
			
		||||
    <title><%= htmlWebpackPlugin.options.title %></title>
 | 
			
		||||
    <link inline rel="icon" type="image/png" href="favicon.png">
 | 
			
		||||
    <% for (const css in htmlWebpackPlugin.files.css) { %>
 | 
			
		||||
    <link inline rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.files.css[css] %>">
 | 
			
		||||
    <% } %>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<% for (const js in htmlWebpackPlugin.files.js) { %>
 | 
			
		||||
<script inline type="text/javascript" src="<%= htmlWebpackPlugin.files.js[js] %>"></script>
 | 
			
		||||
<% } %>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										19
									
								
								html/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								html/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "./node_modules/gts/tsconfig-google.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "jsx": "react",
 | 
			
		||||
    "jsxFactory": "h",
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "noImplicitAny": false,
 | 
			
		||||
    "declaration": false,
 | 
			
		||||
    "experimentalDecorators": true,
 | 
			
		||||
    "strictPropertyInitialization": false,
 | 
			
		||||
    "lib": ["es2019", "dom"],
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src/**/*.tsx",
 | 
			
		||||
    "src/**/*.ts"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										101
									
								
								html/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								html/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const { merge } = require('webpack-merge');
 | 
			
		||||
const ESLintPlugin = require('eslint-webpack-plugin');
 | 
			
		||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
 | 
			
		||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
 | 
			
		||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 | 
			
		||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
 | 
			
		||||
const TerserPlugin = require('terser-webpack-plugin');
 | 
			
		||||
 | 
			
		||||
const devMode = process.env.NODE_ENV !== 'production';
 | 
			
		||||
 | 
			
		||||
const baseConfig = {
 | 
			
		||||
    context: path.resolve(__dirname, 'src'),
 | 
			
		||||
    entry: {
 | 
			
		||||
        app: './index.tsx',
 | 
			
		||||
    },
 | 
			
		||||
    output: {
 | 
			
		||||
        path: path.resolve(__dirname, 'dist'),
 | 
			
		||||
        filename: devMode ? '[name].js' : '[name].[contenthash].js',
 | 
			
		||||
    },
 | 
			
		||||
    module: {
 | 
			
		||||
        rules: [
 | 
			
		||||
            {
 | 
			
		||||
                test: /\.tsx?$/,
 | 
			
		||||
                use: 'ts-loader',
 | 
			
		||||
                exclude: /node_modules/,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                test: /\.s?[ac]ss$/,
 | 
			
		||||
                use: [devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    resolve: {
 | 
			
		||||
        extensions: ['.tsx', '.ts', '.js'],
 | 
			
		||||
    },
 | 
			
		||||
    plugins: [
 | 
			
		||||
        new ESLintPlugin({
 | 
			
		||||
            context: path.resolve(__dirname, '.'),
 | 
			
		||||
            extensions: ['js', 'jsx', 'ts', 'tsx'],
 | 
			
		||||
        }),
 | 
			
		||||
        new CopyWebpackPlugin({
 | 
			
		||||
            patterns: [{ from: './favicon.png', to: '.' }],
 | 
			
		||||
        }),
 | 
			
		||||
        new MiniCssExtractPlugin({
 | 
			
		||||
            filename: devMode ? '[name].css' : '[name].[contenthash].css',
 | 
			
		||||
            chunkFilename: devMode ? '[id].css' : '[id].[contenthash].css',
 | 
			
		||||
        }),
 | 
			
		||||
        new HtmlWebpackPlugin({
 | 
			
		||||
            inject: false,
 | 
			
		||||
            minify: {
 | 
			
		||||
                removeComments: true,
 | 
			
		||||
                collapseWhitespace: true,
 | 
			
		||||
            },
 | 
			
		||||
            title: 'ttyd - Terminal',
 | 
			
		||||
            template: './template.html',
 | 
			
		||||
        }),
 | 
			
		||||
    ],
 | 
			
		||||
    performance: {
 | 
			
		||||
        hints: false,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const devConfig = {
 | 
			
		||||
    mode: 'development',
 | 
			
		||||
    devServer: {
 | 
			
		||||
        static: path.join(__dirname, 'dist'),
 | 
			
		||||
        compress: true,
 | 
			
		||||
        port: 9000,
 | 
			
		||||
        client: {
 | 
			
		||||
            overlay: {
 | 
			
		||||
                errors: true,
 | 
			
		||||
                warnings: false,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        proxy: [
 | 
			
		||||
            {
 | 
			
		||||
                context: ['/token', '/ws'],
 | 
			
		||||
                target: 'http://localhost:7681',
 | 
			
		||||
                ws: true,
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        webSocketServer: {
 | 
			
		||||
            type: 'sockjs',
 | 
			
		||||
            options: {
 | 
			
		||||
                path: '/sockjs-node',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    devtool: 'inline-source-map',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const prodConfig = {
 | 
			
		||||
    mode: 'production',
 | 
			
		||||
    optimization: {
 | 
			
		||||
        minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
 | 
			
		||||
    },
 | 
			
		||||
    devtool: 'source-map',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = merge(baseConfig, devMode ? devConfig : prodConfig);
 | 
			
		||||
							
								
								
									
										8066
									
								
								html/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8066
									
								
								html/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6
									
								
								man/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								man/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
# Building the man page
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go get github.com/cpuguy83/go-md2man
 | 
			
		||||
go-md2man < ttyd.man.md  > ttyd.1
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										353
									
								
								man/ttyd.1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								man/ttyd.1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,353 @@
 | 
			
		||||
.nh
 | 
			
		||||
.TH ttyd 1 "September 2016" ttyd "User Manual"
 | 
			
		||||
 | 
			
		||||
.SH NAME
 | 
			
		||||
.PP
 | 
			
		||||
ttyd - Share your terminal over the web
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH SYNOPSIS
 | 
			
		||||
.PP
 | 
			
		||||
\fBttyd\fP [options] <command> [<arguments...>]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH DESCRIPTION
 | 
			
		||||
.PP
 | 
			
		||||
ttyd is a command-line tool for sharing terminal over the web that runs in *nix and windows systems, with the following features:
 | 
			
		||||
 | 
			
		||||
.RS
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Built on top of Libwebsockets with libuv for speed
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Fully-featured terminal based on Xterm.js with CJK (Chinese, Japanese, Korean) and IME support
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Graphical ZMODEM integration with lrzsz support
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Sixel image output support
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
SSL support based on OpenSSL
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Run any custom command with options
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Basic authentication support and many other custom options
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Cross platform: macOS, Linux, FreeBSD/OpenBSD, OpenWrt/LEDE, Windows
 | 
			
		||||
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH OPTIONS
 | 
			
		||||
.PP
 | 
			
		||||
-p, --port 
 | 
			
		||||
      Port to listen (default: 7681, use \fB\fC0\fR for random port)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-i, --interface 
 | 
			
		||||
      Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-U, --socket-owner
 | 
			
		||||
      User owner of the UNIX domain socket file, when enabled (eg: user:group)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-c, --credential USER[:PASSWORD]
 | 
			
		||||
      Credential for Basic Authentication (format: username:password)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-H, --auth-header 
 | 
			
		||||
      HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-u, --uid 
 | 
			
		||||
      User id to run with
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-g, --gid 
 | 
			
		||||
      Group id to run with
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-s, --signal 
 | 
			
		||||
      Signal to send to the command when exit it (default: 1, SIGHUP)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-w, --cwd 
 | 
			
		||||
      Working directory to be set for the child program
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-a, --url-arg
 | 
			
		||||
      Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-W, --writable
 | 
			
		||||
      Allow clients to write to the TTY (readonly by default)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-t, --client-option 
 | 
			
		||||
      Send option to client (format: key=value), repeat to add more options, see \fBCLIENT OPTIONS\fP for details
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-T, --terminal-type
 | 
			
		||||
      Terminal type to report, default: xterm-256color
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-O, --check-origin
 | 
			
		||||
      Do not allow websocket connection from different origin
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-m, --max-clients
 | 
			
		||||
      Maximum clients to support (default: 0, no limit)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-o, --once
 | 
			
		||||
      Accept only one client and exit on disconnection
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-q, --exit-no-conn
 | 
			
		||||
      Exit on all clients disconnection
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-B, --browser
 | 
			
		||||
      Open terminal with the default system browser
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-I, --index 
 | 
			
		||||
      Custom index.html path
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-b, --base-path
 | 
			
		||||
      Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-P, --ping-interval
 | 
			
		||||
      Websocket ping interval(sec) (default: 5)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-f, --srv-buf-size
 | 
			
		||||
      Maximum chunk of file (in bytes) that can be sent at once, a larger value may improve throughput (default: 4096)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-6, --ipv6
 | 
			
		||||
      Enable IPv6 support
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-S, --ssl
 | 
			
		||||
      Enable SSL
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-C, --ssl-cert 
 | 
			
		||||
      SSL certificate file path
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-K, --ssl-key 
 | 
			
		||||
      SSL key file path
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-A, --ssl-ca 
 | 
			
		||||
      SSL CA file path for client certificate verification
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-d, --debug 
 | 
			
		||||
      Set log level (default: 7)
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-v, --version
 | 
			
		||||
      Print the version and exit
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
-h, --help
 | 
			
		||||
      Print this text and exit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH CLIENT OPTIONS
 | 
			
		||||
.PP
 | 
			
		||||
ttyd has a mechanism to pass server side command-line arguments to the browser page which is called \fBclient options\fP:
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
-t, --client-option     Send option to client (format: key=value), repeat to add more options
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.SH Basic usage
 | 
			
		||||
.RS
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t rendererType=canvas\fR: use the \fB\fCcanvas\fR renderer for xterm.js (default: \fB\fCwebgl\fR)
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t disableLeaveAlert=true\fR: disable the leave page alert
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t disableResizeOverlay=true\fR: disable the terminal resize overlay
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t disableReconnect=true\fR: prevent the terminal from reconnecting on connection error/close
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t enableZmodem=true\fR: enable ZMODEM
 | 
			
		||||
\[la]https://en.wikipedia.org/wiki/ZMODEM\[ra] / lrzsz
 | 
			
		||||
\[la]https://ohse.de/uwe/software/lrzsz.html\[ra] file transfer support
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t enableTrzsz=true\fR: enable trzsz
 | 
			
		||||
\[la]https://trzsz.github.io\[ra] file transfer support
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t enableSixel=true\fR: enable Sixel
 | 
			
		||||
\[la]https://en.wikipedia.org/wiki/Sixel\[ra] image output support (Usage
 | 
			
		||||
\[la]https://saitoha.github.io/libsixel/\[ra])
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t closeOnDisconnect=true\fR: close the terminal on disconnection, this will disable reconnect
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t titleFixed=hello\fR: set a fixed title for the browser window
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t fontSize=20\fR: change the font size of the terminal
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t unicodeVersion=11\fR: set xterm unicode support level (default: 11, use 6 to disable unicode addon)
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t trzszDragInitTimeout=3000\fR: set the timeout in milliseconds for initializing drag and drop files to upload. (default: 3000)
 | 
			
		||||
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.SH Advanced usage
 | 
			
		||||
.PP
 | 
			
		||||
You can use the client option to change all the settings of xterm defined in ITerminalOptions
 | 
			
		||||
\[la]https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/\[ra], examples:
 | 
			
		||||
 | 
			
		||||
.RS
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t cursorStyle=bar\fR: set cursor style to \fB\fCbar\fR
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t lineHeight=1.5\fR: set line-height to \fB\fC1.5\fR
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
\fB\fC-t 'theme={"background": "green"}'\fR: set background color to \fB\fCgreen\fR
 | 
			
		||||
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
to try the example options above, run:
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
ttyd -t cursorStyle=bar -t lineHeight=1.5 -t 'theme={"background": "green"}' bash
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH EXAMPLES
 | 
			
		||||
.PP
 | 
			
		||||
ttyd starts web server at port 7681 by default, you can use the -p option to change it, the command will be started with arguments as options. For example, run:
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
ttyd -p 8080 bash -x
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
Then open http://localhost:8080 with a browser, you will get a bash shell with debug mode enabled. More examples:
 | 
			
		||||
 | 
			
		||||
.RS
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
If you want to login with your system accounts on the web browser, run \fB\fCttyd login\fR\&.
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
You can even run a non-shell command like vim, try: \fB\fCttyd vim\fR, the web browser will show you a vim editor.
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Sharing single process with multiple clients: \fB\fCttyd tmux new -A -s ttyd vim\fR, run \fB\fCtmux new -A -s ttyd\fR to connect to the tmux session from terminal.
 | 
			
		||||
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH SSL how-to
 | 
			
		||||
.PP
 | 
			
		||||
Generate SSL CA and self signed server/client certificates:
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
# CA certificate (FQDN must be different from server/client)
 | 
			
		||||
openssl genrsa -out ca.key 2048
 | 
			
		||||
openssl req -new -x509 -days 365 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=Acme Root CA" -out ca.crt
 | 
			
		||||
 | 
			
		||||
# server certificate (for multiple domains, change subjectAltName to: DNS:example.com,DNS:www.example.com)
 | 
			
		||||
openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=localhost" -out server.csr
 | 
			
		||||
openssl x509 -sha256 -req -extfile <(printf "subjectAltName=DNS:localhost") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
 | 
			
		||||
 | 
			
		||||
# client certificate (the p12/pem format may be useful for some clients)
 | 
			
		||||
openssl req -newkey rsa:2048 -nodes -keyout client.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=client" -out client.csr
 | 
			
		||||
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
 | 
			
		||||
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
 | 
			
		||||
openssl pkcs12 -in client.p12 -out client.pem -clcerts
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
Then start ttyd:
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
ttyd --ssl --ssl-cert server.crt --ssl-key server.key --ssl-ca ca.crt bash
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
You may want to test the client certificate verification with \fIcurl\fP(1):
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
curl --insecure --cert client.p12[:password] -v https://localhost:7681
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
If you don't want to enable client certificate verification, remove the \fB\fC--ssl-ca\fR option.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH Docker and ttyd
 | 
			
		||||
.PP
 | 
			
		||||
Docker containers are jailed environments which are more secure, this is useful for protecting the host system, you may use ttyd with docker like this:
 | 
			
		||||
 | 
			
		||||
.RS
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Sharing single docker container with multiple clients: docker run -it --rm -p 7681:7681 tsl0922/ttyd.
 | 
			
		||||
.IP \(bu 2
 | 
			
		||||
Creating new docker container for each client: ttyd docker run -it --rm ubuntu.
 | 
			
		||||
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH Nginx reverse proxy
 | 
			
		||||
.PP
 | 
			
		||||
Sample config to proxy ttyd under the \fB\fC/ttyd\fR path:
 | 
			
		||||
 | 
			
		||||
.PP
 | 
			
		||||
.RS
 | 
			
		||||
 | 
			
		||||
.nf
 | 
			
		||||
location ~ ^/ttyd(.*)$ {
 | 
			
		||||
    proxy_http_version 1.1;
 | 
			
		||||
    proxy_set_header Host $host;
 | 
			
		||||
    proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
    proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
    proxy_set_header Connection "upgrade";
 | 
			
		||||
    proxy_pass http://127.0.0.1:7681/$1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fi
 | 
			
		||||
.RE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.SH AUTHOR
 | 
			
		||||
.PP
 | 
			
		||||
Shuanglei Tao <tsl0922@gmail.com> Visit https://github.com/tsl0922/ttyd to get more information and report bugs.
 | 
			
		||||
							
								
								
									
										218
									
								
								man/ttyd.man.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								man/ttyd.man.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
ttyd 1 "September 2016" ttyd "User Manual"
 | 
			
		||||
==================================================
 | 
			
		||||
 | 
			
		||||
# NAME
 | 
			
		||||
  ttyd - Share your terminal over the web
 | 
			
		||||
 | 
			
		||||
# SYNOPSIS
 | 
			
		||||
  **ttyd** [options] \<command\> [\<arguments...\>]
 | 
			
		||||
 | 
			
		||||
# DESCRIPTION
 | 
			
		||||
  ttyd is a command-line tool for sharing terminal over the web that runs in *nix and windows systems, with the following features:
 | 
			
		||||
 | 
			
		||||
  - Built on top of Libwebsockets with libuv for speed
 | 
			
		||||
  - Fully-featured terminal based on Xterm.js with CJK (Chinese, Japanese, Korean) and IME support
 | 
			
		||||
  - Graphical ZMODEM integration with lrzsz support
 | 
			
		||||
  - Sixel image output support
 | 
			
		||||
  - SSL support based on OpenSSL
 | 
			
		||||
  - Run any custom command with options
 | 
			
		||||
  - Basic authentication support and many other custom options
 | 
			
		||||
  - Cross platform: macOS, Linux, FreeBSD/OpenBSD, OpenWrt/LEDE, Windows
 | 
			
		||||
 | 
			
		||||
# OPTIONS
 | 
			
		||||
  -p, --port <port>
 | 
			
		||||
      Port to listen (default: 7681, use `0` for random port)
 | 
			
		||||
 | 
			
		||||
  -i, --interface <interface>
 | 
			
		||||
      Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
 | 
			
		||||
 | 
			
		||||
  -U, --socket-owner
 | 
			
		||||
      User owner of the UNIX domain socket file, when enabled (eg: user:group)
 | 
			
		||||
 | 
			
		||||
  -c, --credential USER[:PASSWORD]
 | 
			
		||||
      Credential for Basic Authentication (format: username:password)
 | 
			
		||||
 | 
			
		||||
  -H, --auth-header <name>
 | 
			
		||||
      HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication
 | 
			
		||||
 | 
			
		||||
  -u, --uid <uid>
 | 
			
		||||
      User id to run with
 | 
			
		||||
 | 
			
		||||
  -g, --gid <gid>
 | 
			
		||||
      Group id to run with
 | 
			
		||||
 | 
			
		||||
  -s, --signal <signal string>
 | 
			
		||||
      Signal to send to the command when exit it (default: 1, SIGHUP)
 | 
			
		||||
 | 
			
		||||
  -w, --cwd <path>
 | 
			
		||||
      Working directory to be set for the child program
 | 
			
		||||
 | 
			
		||||
  -a, --url-arg
 | 
			
		||||
      Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
 | 
			
		||||
 | 
			
		||||
  -W, --writable
 | 
			
		||||
      Allow clients to write to the TTY (readonly by default)
 | 
			
		||||
 | 
			
		||||
  -t, --client-option <key=value>
 | 
			
		||||
      Send option to client (format: key=value), repeat to add more options, see **CLIENT OPTIONS** for details
 | 
			
		||||
 | 
			
		||||
  -T, --terminal-type
 | 
			
		||||
      Terminal type to report, default: xterm-256color
 | 
			
		||||
 | 
			
		||||
  -O, --check-origin
 | 
			
		||||
      Do not allow websocket connection from different origin
 | 
			
		||||
 | 
			
		||||
  -m, --max-clients
 | 
			
		||||
      Maximum clients to support (default: 0, no limit)
 | 
			
		||||
 | 
			
		||||
  -o, --once
 | 
			
		||||
      Accept only one client and exit on disconnection
 | 
			
		||||
 | 
			
		||||
  -q, --exit-no-conn
 | 
			
		||||
      Exit on all clients disconnection
 | 
			
		||||
 | 
			
		||||
  -B, --browser
 | 
			
		||||
      Open terminal with the default system browser
 | 
			
		||||
 | 
			
		||||
  -I, --index <index file>
 | 
			
		||||
      Custom index.html path
 | 
			
		||||
  
 | 
			
		||||
  -b, --base-path
 | 
			
		||||
      Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)
 | 
			
		||||
 | 
			
		||||
  -P, --ping-interval
 | 
			
		||||
      Websocket ping interval(sec) (default: 5)
 | 
			
		||||
 | 
			
		||||
  -f, --srv-buf-size
 | 
			
		||||
      Maximum chunk of file (in bytes) that can be sent at once, a larger value may improve throughput (default: 4096)
 | 
			
		||||
 | 
			
		||||
  -6, --ipv6
 | 
			
		||||
      Enable IPv6 support
 | 
			
		||||
 | 
			
		||||
  -S, --ssl
 | 
			
		||||
      Enable SSL
 | 
			
		||||
 | 
			
		||||
  -C, --ssl-cert <cert path>
 | 
			
		||||
      SSL certificate file path
 | 
			
		||||
 | 
			
		||||
  -K, --ssl-key <key path>
 | 
			
		||||
      SSL key file path
 | 
			
		||||
 | 
			
		||||
  -A, --ssl-ca <ca path>
 | 
			
		||||
      SSL CA file path for client certificate verification
 | 
			
		||||
 | 
			
		||||
  -d, --debug <level>
 | 
			
		||||
      Set log level (default: 7)
 | 
			
		||||
 | 
			
		||||
  -v, --version
 | 
			
		||||
      Print the version and exit
 | 
			
		||||
 | 
			
		||||
  -h, --help
 | 
			
		||||
      Print this text and exit
 | 
			
		||||
 | 
			
		||||
# CLIENT OPTIONS
 | 
			
		||||
ttyd has a mechanism to pass server side command-line arguments to the browser page which is called **client options**:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
-t, --client-option     Send option to client (format: key=value), repeat to add more options
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Basic usage
 | 
			
		||||
 | 
			
		||||
- `-t rendererType=canvas`: use the `canvas` renderer for xterm.js (default: `webgl`)
 | 
			
		||||
- `-t disableLeaveAlert=true`: disable the leave page alert
 | 
			
		||||
- `-t disableResizeOverlay=true`: disable the terminal resize overlay
 | 
			
		||||
- `-t disableReconnect=true`: prevent the terminal from reconnecting on connection error/close
 | 
			
		||||
- `-t enableZmodem=true`: enable [ZMODEM](https://en.wikipedia.org/wiki/ZMODEM) / [lrzsz](https://ohse.de/uwe/software/lrzsz.html) file transfer support
 | 
			
		||||
- `-t enableTrzsz=true`: enable [trzsz](https://trzsz.github.io) file transfer support
 | 
			
		||||
- `-t enableSixel=true`: enable [Sixel](https://en.wikipedia.org/wiki/Sixel) image output support ([Usage](https://saitoha.github.io/libsixel/))
 | 
			
		||||
- `-t closeOnDisconnect=true`: close the terminal on disconnection, this will disable reconnect
 | 
			
		||||
- `-t titleFixed=hello`: set a fixed title for the browser window
 | 
			
		||||
- `-t fontSize=20`: change the font size of the terminal
 | 
			
		||||
- `-t unicodeVersion=11`: set xterm unicode support level (default: 11, use 6 to disable unicode addon)
 | 
			
		||||
- `-t trzszDragInitTimeout=3000`: set the timeout in milliseconds for initializing drag and drop files to upload. (default: 3000) 
 | 
			
		||||
 | 
			
		||||
## Advanced usage
 | 
			
		||||
 | 
			
		||||
You can use the client option to change all the settings of xterm defined in [ITerminalOptions](https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/), examples:
 | 
			
		||||
 | 
			
		||||
- `-t cursorStyle=bar`: set cursor style to `bar`
 | 
			
		||||
- `-t lineHeight=1.5`: set line-height to `1.5`
 | 
			
		||||
- `-t 'theme={"background": "green"}'`: set background color to `green`
 | 
			
		||||
 | 
			
		||||
to try the example options above, run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
ttyd -t cursorStyle=bar -t lineHeight=1.5 -t 'theme={"background": "green"}' bash
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# EXAMPLES
 | 
			
		||||
  ttyd starts web server at port 7681 by default, you can use the -p option to change it, the command will be started with arguments as options. For example, run:
 | 
			
		||||
  
 | 
			
		||||
```  
 | 
			
		||||
ttyd -p 8080 bash -x
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
  Then open http://localhost:8080 with a browser, you will get a bash shell with debug mode enabled. More examples:
 | 
			
		||||
 | 
			
		||||
  - If you want to login with your system accounts on the web browser, run `ttyd login`.
 | 
			
		||||
  - You can even run a non-shell command like vim, try: `ttyd vim`, the web browser will show you a vim editor.
 | 
			
		||||
  - Sharing single process with multiple clients: `ttyd tmux new -A -s ttyd vim`, run `tmux new -A -s ttyd` to connect to the tmux session from terminal.
 | 
			
		||||
 | 
			
		||||
# SSL how-to
 | 
			
		||||
  Generate SSL CA and self signed server/client certificates:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
# CA certificate (FQDN must be different from server/client)
 | 
			
		||||
openssl genrsa -out ca.key 2048
 | 
			
		||||
openssl req -new -x509 -days 365 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=Acme Root CA" -out ca.crt
 | 
			
		||||
 | 
			
		||||
# server certificate (for multiple domains, change subjectAltName to: DNS:example.com,DNS:www.example.com)
 | 
			
		||||
openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=localhost" -out server.csr
 | 
			
		||||
openssl x509 -sha256 -req -extfile <(printf "subjectAltName=DNS:localhost") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
 | 
			
		||||
 | 
			
		||||
# client certificate (the p12/pem format may be useful for some clients)
 | 
			
		||||
openssl req -newkey rsa:2048 -nodes -keyout client.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=client" -out client.csr
 | 
			
		||||
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
 | 
			
		||||
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
 | 
			
		||||
openssl pkcs12 -in client.p12 -out client.pem -clcerts
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
  Then start ttyd:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ttyd --ssl --ssl-cert server.crt --ssl-key server.key --ssl-ca ca.crt bash
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
  You may want to test the client certificate verification with *curl*(1):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
curl --insecure --cert client.p12[:password] -v https://localhost:7681
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
  If you don't want to enable client certificate verification, remove the `--ssl-ca` option.
 | 
			
		||||
 | 
			
		||||
# Docker and ttyd
 | 
			
		||||
  Docker containers are jailed environments which are more secure, this is useful for protecting the host system, you may use ttyd with docker like this:
 | 
			
		||||
 | 
			
		||||
  - Sharing single docker container with multiple clients: docker run -it --rm -p 7681:7681 tsl0922/ttyd.
 | 
			
		||||
  - Creating new docker container for each client: ttyd docker run -it --rm ubuntu.
 | 
			
		||||
 | 
			
		||||
# Nginx reverse proxy
 | 
			
		||||
 | 
			
		||||
Sample config to proxy ttyd under the `/ttyd` path:
 | 
			
		||||
 | 
			
		||||
```nginx
 | 
			
		||||
location ~ ^/ttyd(.*)$ {
 | 
			
		||||
    proxy_http_version 1.1;
 | 
			
		||||
    proxy_set_header Host $host;
 | 
			
		||||
    proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
    proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
    proxy_set_header Connection "upgrade";
 | 
			
		||||
    proxy_pass http://127.0.0.1:7681/$1;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# AUTHOR
 | 
			
		||||
  Shuanglei Tao \<tsl0922@gmail.com\> Visit https://github.com/tsl0922/ttyd to get more information and report bugs.
 | 
			
		||||
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ttyd",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								screenshot.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								screenshot.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 125 KiB  | 
							
								
								
									
										191
									
								
								scripts/cross-build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										191
									
								
								scripts/cross-build.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,191 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
#
 | 
			
		||||
# Example:
 | 
			
		||||
#         env BUILD_TARGET=mips ./scripts/cross-build.sh
 | 
			
		||||
#
 | 
			
		||||
set -eo pipefail
 | 
			
		||||
 | 
			
		||||
CROSS_ROOT="${CROSS_ROOT:-/opt/cross}"
 | 
			
		||||
STAGE_ROOT="${STAGE_ROOT:-/opt/stage}"
 | 
			
		||||
BUILD_ROOT="${BUILD_ROOT:-/opt/build}"
 | 
			
		||||
BUILD_TARGET="${BUILD_TARGET:-x86_64}"
 | 
			
		||||
 | 
			
		||||
ZLIB_VERSION="${ZLIB_VERSION:-1.3.1}"
 | 
			
		||||
JSON_C_VERSION="${JSON_C_VERSION:-0.17}"
 | 
			
		||||
MBEDTLS_VERSION="${MBEDTLS_VERSION:-2.28.5}"
 | 
			
		||||
LIBUV_VERSION="${LIBUV_VERSION:-1.44.2}"
 | 
			
		||||
LIBWEBSOCKETS_VERSION="${LIBWEBSOCKETS_VERSION:-4.3.3}"
 | 
			
		||||
 | 
			
		||||
build_zlib() {
 | 
			
		||||
    echo "=== Building zlib-${ZLIB_VERSION} (${TARGET})..."
 | 
			
		||||
    curl -fSsLo- "https://zlib.net/zlib-${ZLIB_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
 | 
			
		||||
    pushd "${BUILD_DIR}"/zlib-"${ZLIB_VERSION}"
 | 
			
		||||
        env CHOST="${TARGET}" ./configure --static --archs="-fPIC" --prefix="${STAGE_DIR}"
 | 
			
		||||
        make -j"$(nproc)" install
 | 
			
		||||
    popd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_json-c() {
 | 
			
		||||
    echo "=== Building json-c-${JSON_C_VERSION} (${TARGET})..."
 | 
			
		||||
    curl -fSsLo- "https://s3.amazonaws.com/json-c_releases/releases/json-c-${JSON_C_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
 | 
			
		||||
    pushd "${BUILD_DIR}/json-c-${JSON_C_VERSION}"
 | 
			
		||||
        rm -rf build && mkdir -p build && cd build
 | 
			
		||||
        cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
 | 
			
		||||
            -DCMAKE_BUILD_TYPE=RELEASE \
 | 
			
		||||
            -DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \
 | 
			
		||||
            -DBUILD_SHARED_LIBS=OFF \
 | 
			
		||||
            -DBUILD_TESTING=OFF \
 | 
			
		||||
            -DDISABLE_THREAD_LOCAL_STORAGE=ON \
 | 
			
		||||
            ..
 | 
			
		||||
        make -j"$(nproc)" install
 | 
			
		||||
    popd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_mbedtls() {
 | 
			
		||||
    echo "=== Building mbedtls-${MBEDTLS_VERSION} (${TARGET})..."
 | 
			
		||||
    curl -fSsLo- "https://github.com/ARMmbed/mbedtls/archive/v${MBEDTLS_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
 | 
			
		||||
    pushd "${BUILD_DIR}/mbedtls-${MBEDTLS_VERSION}"
 | 
			
		||||
        rm -rf build && mkdir -p build && cd build
 | 
			
		||||
        cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
 | 
			
		||||
            -DCMAKE_BUILD_TYPE=RELEASE \
 | 
			
		||||
            -DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \
 | 
			
		||||
            -DENABLE_TESTING=OFF \
 | 
			
		||||
            ..
 | 
			
		||||
        make -j"$(nproc)" install
 | 
			
		||||
    popd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_libuv() {
 | 
			
		||||
    echo "=== Building libuv-${LIBUV_VERSION} (${TARGET})..."
 | 
			
		||||
    curl -fSsLo- "https://dist.libuv.org/dist/v${LIBUV_VERSION}/libuv-v${LIBUV_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
 | 
			
		||||
    pushd "${BUILD_DIR}/libuv-v${LIBUV_VERSION}"
 | 
			
		||||
        ./autogen.sh
 | 
			
		||||
        env CFLAGS=-fPIC ./configure --disable-shared --enable-static --prefix="${STAGE_DIR}" --host="${TARGET}"
 | 
			
		||||
        make -j"$(nproc)" install
 | 
			
		||||
    popd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
install_cmake_cross_file() {
 | 
			
		||||
    cat << EOF > "${BUILD_DIR}/cross-${TARGET}.cmake"
 | 
			
		||||
SET(CMAKE_SYSTEM_NAME $1)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_C_COMPILER "${TARGET}-gcc")
 | 
			
		||||
set(CMAKE_CXX_COMPILER "${TARGET}-g++")
 | 
			
		||||
 | 
			
		||||
set(CMAKE_FIND_ROOT_PATH "${STAGE_DIR}")
 | 
			
		||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
 | 
			
		||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
 | 
			
		||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
 | 
			
		||||
 | 
			
		||||
set(OPENSSL_USE_STATIC_LIBS TRUE)
 | 
			
		||||
EOF
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_libwebsockets() {
 | 
			
		||||
    echo "=== Building libwebsockets-${LIBWEBSOCKETS_VERSION} (${TARGET})..."
 | 
			
		||||
    curl -fSsLo- "https://github.com/warmcat/libwebsockets/archive/v${LIBWEBSOCKETS_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
 | 
			
		||||
    pushd "${BUILD_DIR}/libwebsockets-${LIBWEBSOCKETS_VERSION}"
 | 
			
		||||
        sed -i 's/ websockets_shared//g' cmake/libwebsockets-config.cmake.in
 | 
			
		||||
        sed -i 's/ OR PC_OPENSSL_FOUND//g' lib/tls/CMakeLists.txt
 | 
			
		||||
        sed -i '/PC_OPENSSL/d' lib/tls/CMakeLists.txt
 | 
			
		||||
        rm -rf build && mkdir -p build && cd build
 | 
			
		||||
        cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
 | 
			
		||||
            -DCMAKE_BUILD_TYPE=RELEASE \
 | 
			
		||||
            -DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \
 | 
			
		||||
            -DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \
 | 
			
		||||
            -DCMAKE_EXE_LINKER_FLAGS="-static" \
 | 
			
		||||
            -DLWS_WITHOUT_TESTAPPS=ON \
 | 
			
		||||
            -DLWS_WITH_MBEDTLS=ON \
 | 
			
		||||
            -DLWS_WITH_LIBUV=ON \
 | 
			
		||||
            -DLWS_STATIC_PIC=ON \
 | 
			
		||||
            -DLWS_WITH_SHARED=OFF \
 | 
			
		||||
            -DLWS_UNIX_SOCK=ON \
 | 
			
		||||
            -DLWS_IPV6=ON \
 | 
			
		||||
            -DLWS_ROLE_RAW_FILE=OFF \
 | 
			
		||||
            -DLWS_WITH_HTTP2=ON \
 | 
			
		||||
            -DLWS_WITH_HTTP_BASIC_AUTH=OFF \
 | 
			
		||||
            -DLWS_WITH_UDP=OFF \
 | 
			
		||||
            -DLWS_WITHOUT_CLIENT=ON \
 | 
			
		||||
            -DLWS_WITHOUT_EXTENSIONS=OFF \
 | 
			
		||||
            -DLWS_WITH_LEJP=OFF \
 | 
			
		||||
            -DLWS_WITH_LEJP_CONF=OFF \
 | 
			
		||||
            -DLWS_WITH_LWSAC=OFF \
 | 
			
		||||
            -DLWS_WITH_SEQUENCER=OFF \
 | 
			
		||||
            ..
 | 
			
		||||
        make -j"$(nproc)" install
 | 
			
		||||
    popd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_ttyd() {
 | 
			
		||||
    echo "=== Building ttyd (${TARGET})..."
 | 
			
		||||
    rm -rf build && mkdir -p build && cd build
 | 
			
		||||
    cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
 | 
			
		||||
        -DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \
 | 
			
		||||
        -DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \
 | 
			
		||||
        -DCMAKE_C_FLAGS="-Os -ffunction-sections -fdata-sections -fno-unwind-tables -fno-asynchronous-unwind-tables -flto" \
 | 
			
		||||
        -DCMAKE_EXE_LINKER_FLAGS="-static -no-pie -Wl,-s -Wl,-Bsymbolic -Wl,--gc-sections" \
 | 
			
		||||
        -DCMAKE_BUILD_TYPE=RELEASE \
 | 
			
		||||
        ..
 | 
			
		||||
    make install
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build() {
 | 
			
		||||
    TARGET="$1"
 | 
			
		||||
    ALIAS="$2"
 | 
			
		||||
    STAGE_DIR="${STAGE_ROOT}/${TARGET}"
 | 
			
		||||
    BUILD_DIR="${BUILD_ROOT}/${TARGET}"
 | 
			
		||||
    MUSL_CC_URL="https://github.com/tsl0922/musl-toolchains/releases/download/2021-11-23"
 | 
			
		||||
    COMPONENTS="1"
 | 
			
		||||
    SYSTEM="Linux"
 | 
			
		||||
 | 
			
		||||
    if [ "$ALIAS" = "win32" ]; then
 | 
			
		||||
        COMPONENTS=2
 | 
			
		||||
        SYSTEM="Windows"
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    echo "=== Installing toolchain ${ALIAS} (${TARGET})..."
 | 
			
		||||
 | 
			
		||||
    mkdir -p "${CROSS_ROOT}" && export PATH="${PATH}:${CROSS_ROOT}/bin"
 | 
			
		||||
    curl -fSsLo- "${MUSL_CC_URL}/${TARGET}-cross.tgz" | tar xz -C "${CROSS_ROOT}" --strip-components=${COMPONENTS}
 | 
			
		||||
 | 
			
		||||
    echo "=== Building target ${ALIAS} (${TARGET})..."
 | 
			
		||||
 | 
			
		||||
    rm -rf "${STAGE_DIR}" "${BUILD_DIR}"
 | 
			
		||||
    mkdir -p "${STAGE_DIR}" "${BUILD_DIR}"
 | 
			
		||||
    export PKG_CONFIG_PATH="${STAGE_DIR}/lib/pkgconfig"
 | 
			
		||||
 | 
			
		||||
    install_cmake_cross_file ${SYSTEM}
 | 
			
		||||
 | 
			
		||||
    build_zlib
 | 
			
		||||
    build_json-c
 | 
			
		||||
    build_libuv
 | 
			
		||||
    build_mbedtls
 | 
			
		||||
    build_libwebsockets
 | 
			
		||||
    build_ttyd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
case ${BUILD_TARGET} in
 | 
			
		||||
    amd64) BUILD_TARGET="x86_64" ;;
 | 
			
		||||
    arm64) BUILD_TARGET="aarch64" ;;
 | 
			
		||||
    armv7) BUILD_TARGET="armv7l" ;;
 | 
			
		||||
esac
 | 
			
		||||
 | 
			
		||||
case ${BUILD_TARGET} in
 | 
			
		||||
    i686|x86_64|aarch64|mips|mipsel|mips64|mips64el|s390x)
 | 
			
		||||
        build "${BUILD_TARGET}-linux-musl" "${BUILD_TARGET}"
 | 
			
		||||
        ;;
 | 
			
		||||
    arm)
 | 
			
		||||
        build "${BUILD_TARGET}-linux-musleabi" "${BUILD_TARGET}"
 | 
			
		||||
        ;;
 | 
			
		||||
    armhf)
 | 
			
		||||
        build arm-linux-musleabihf "${BUILD_TARGET}"
 | 
			
		||||
        ;;
 | 
			
		||||
    armv7l)
 | 
			
		||||
        build armv7l-linux-musleabihf "${BUILD_TARGET}"
 | 
			
		||||
        ;;
 | 
			
		||||
    win32)
 | 
			
		||||
        build x86_64-w64-mingw32 "${BUILD_TARGET}"
 | 
			
		||||
        ;;
 | 
			
		||||
    *)
 | 
			
		||||
        echo "unknown cross target: ${BUILD_TARGET}" && exit 1
 | 
			
		||||
esac
 | 
			
		||||
							
								
								
									
										27
									
								
								scripts/mingw-build.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								scripts/mingw-build.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
set -eo pipefail
 | 
			
		||||
 | 
			
		||||
build_libwebsockets() {
 | 
			
		||||
  svn co https://github.com/msys2/MINGW-packages/trunk/mingw-w64-libwebsockets
 | 
			
		||||
  sed -i 's/openssl/mbedtls/' mingw-w64-libwebsockets/PKGBUILD
 | 
			
		||||
  sed -i '/-DCMAKE_INSTALL_PREFIX=${MINGW_PREFIX}/a \    -DLWS_WITH_MBEDTLS=ON \\' mingw-w64-libwebsockets/PKGBUILD
 | 
			
		||||
  sed -i '/-DCMAKE_INSTALL_PREFIX=${MINGW_PREFIX}/a \    -DLWS_WITH_LIBUV=ON \\' mingw-w64-libwebsockets/PKGBUILD
 | 
			
		||||
  pushd mingw-w64-libwebsockets
 | 
			
		||||
    makepkg-mingw --cleanbuild --syncdeps --force --noconfirm
 | 
			
		||||
    pacman -U *.pkg.tar.zst --noconfirm
 | 
			
		||||
  popd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_libwebsockets
 | 
			
		||||
 | 
			
		||||
# workaround for the lib name change
 | 
			
		||||
cp ${MINGW_PREFIX}/lib/libuv_a.a ${MINGW_PREFIX}/lib/libuv.a
 | 
			
		||||
 | 
			
		||||
rm -rf build && mkdir -p build && cd build
 | 
			
		||||
cmake -DCMAKE_BUILD_TYPE=RELEASE \
 | 
			
		||||
    -DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \
 | 
			
		||||
    -DCMAKE_C_FLAGS="-Os -ffunction-sections -fdata-sections -fno-unwind-tables -fno-asynchronous-unwind-tables -flto" \
 | 
			
		||||
    -DCMAKE_EXE_LINKER_FLAGS="-static -no-pie -Wl,-s -Wl,-Bsymbolic -Wl,--gc-sections" \
 | 
			
		||||
    ..
 | 
			
		||||
cmake --build .
 | 
			
		||||
							
								
								
									
										49
									
								
								snap/snapcraft.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								snap/snapcraft.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
name: ttyd
 | 
			
		||||
adopt-info: ttyd
 | 
			
		||||
summary: Share your terminal over the web
 | 
			
		||||
description: |
 | 
			
		||||
 ttyd is a simple command-line tool for sharing terminal over the web
 | 
			
		||||
 | 
			
		||||
grade: stable
 | 
			
		||||
confinement: classic
 | 
			
		||||
base: core20
 | 
			
		||||
compression: lzo
 | 
			
		||||
 | 
			
		||||
license: MIT
 | 
			
		||||
 | 
			
		||||
assumes:
 | 
			
		||||
  - command-chain
 | 
			
		||||
 | 
			
		||||
apps:
 | 
			
		||||
  ttyd:
 | 
			
		||||
    command: usr/bin/ttyd
 | 
			
		||||
    command-chain: 
 | 
			
		||||
      - bin/homeishome-launch       
 | 
			
		||||
 | 
			
		||||
parts:
 | 
			
		||||
  ttyd:
 | 
			
		||||
    source: https://github.com/tsl0922/ttyd
 | 
			
		||||
    source-type: git
 | 
			
		||||
    plugin: cmake
 | 
			
		||||
    cmake-parameters:
 | 
			
		||||
      - -DCMAKE_INSTALL_PREFIX=/usr
 | 
			
		||||
    build-environment:
 | 
			
		||||
      - LDFLAGS: "-pthread"
 | 
			
		||||
      
 | 
			
		||||
    override-pull: |
 | 
			
		||||
      snapcraftctl pull
 | 
			
		||||
      snapcraftctl set-version "$(git describe --tags | sed 's/^v//' | cut -d "-" -f1)"
 | 
			
		||||
      
 | 
			
		||||
    build-packages:
 | 
			
		||||
      - build-essential 
 | 
			
		||||
      - libjson-c-dev 
 | 
			
		||||
      - libwebsockets-dev
 | 
			
		||||
      
 | 
			
		||||
    stage-packages:
 | 
			
		||||
      - libjson-c4
 | 
			
		||||
      - libwebsockets15
 | 
			
		||||
      
 | 
			
		||||
  homeishome-launch:
 | 
			
		||||
    plugin: nil
 | 
			
		||||
    stage-snaps:
 | 
			
		||||
      - homeishome-launch  
 | 
			
		||||
							
								
								
									
										16460
									
								
								src/html.h
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16460
									
								
								src/html.h
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										240
									
								
								src/http.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/http.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,240 @@
 | 
			
		||||
#include <libwebsockets.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
#include <zlib.h>
 | 
			
		||||
 | 
			
		||||
#include "html.h"
 | 
			
		||||
#include "server.h"
 | 
			
		||||
#include "utils.h"
 | 
			
		||||
 | 
			
		||||
enum { AUTH_OK, AUTH_FAIL, AUTH_ERROR };
 | 
			
		||||
 | 
			
		||||
static char *html_cache = NULL;
 | 
			
		||||
static size_t html_cache_len = 0;
 | 
			
		||||
 | 
			
		||||
static int send_unauthorized(struct lws *wsi, unsigned int code, enum lws_token_indexes header) {
 | 
			
		||||
  unsigned char buffer[1024 + LWS_PRE], *p, *end;
 | 
			
		||||
  p = buffer + LWS_PRE;
 | 
			
		||||
  end = p + sizeof(buffer) - LWS_PRE;
 | 
			
		||||
 | 
			
		||||
  if (lws_add_http_header_status(wsi, code, &p, end) ||
 | 
			
		||||
      lws_add_http_header_by_token(wsi, header, (unsigned char *)"Basic realm=\"ttyd\"", 18, &p, end) ||
 | 
			
		||||
      lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
 | 
			
		||||
      lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
 | 
			
		||||
    return AUTH_FAIL;
 | 
			
		||||
 | 
			
		||||
  return lws_http_transaction_completed(wsi) ? AUTH_FAIL : AUTH_ERROR;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static int check_auth(struct lws *wsi, struct pss_http *pss) {
 | 
			
		||||
  if (server->auth_header != NULL) {
 | 
			
		||||
    if (lws_hdr_custom_length(wsi, server->auth_header, strlen(server->auth_header)) > 0) return AUTH_OK;
 | 
			
		||||
    return send_unauthorized(wsi, HTTP_STATUS_PROXY_AUTH_REQUIRED, WSI_TOKEN_HTTP_PROXY_AUTHENTICATE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if(server->credential != NULL) {
 | 
			
		||||
    char buf[256];
 | 
			
		||||
    int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
 | 
			
		||||
    if (len >= 7 && strstr(buf, "Basic ")) {
 | 
			
		||||
      if (!strcmp(buf + 6, server->credential)) return AUTH_OK;
 | 
			
		||||
    }
 | 
			
		||||
    return send_unauthorized(wsi, HTTP_STATUS_UNAUTHORIZED, WSI_TOKEN_HTTP_WWW_AUTHENTICATE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return AUTH_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool accept_gzip(struct lws *wsi) {
 | 
			
		||||
  char buf[256];
 | 
			
		||||
  int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_ACCEPT_ENCODING);
 | 
			
		||||
  return len > 0 && strstr(buf, "gzip") != NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool uncompress_html(char **output, size_t *output_len) {
 | 
			
		||||
  if (html_cache == NULL || html_cache_len == 0) {
 | 
			
		||||
    z_stream stream;
 | 
			
		||||
    memset(&stream, 0, sizeof(stream));
 | 
			
		||||
    if (inflateInit2(&stream, 16 + 15) != Z_OK) return false;
 | 
			
		||||
 | 
			
		||||
    html_cache_len = index_html_size;
 | 
			
		||||
    html_cache = xmalloc(html_cache_len);
 | 
			
		||||
 | 
			
		||||
    stream.avail_in = index_html_len;
 | 
			
		||||
    stream.avail_out = html_cache_len;
 | 
			
		||||
    stream.next_in = (void *)index_html;
 | 
			
		||||
    stream.next_out = (void *)html_cache;
 | 
			
		||||
 | 
			
		||||
    int ret = inflate(&stream, Z_SYNC_FLUSH);
 | 
			
		||||
    inflateEnd(&stream);
 | 
			
		||||
    if (ret != Z_STREAM_END) {
 | 
			
		||||
      free(html_cache);
 | 
			
		||||
      html_cache = NULL;
 | 
			
		||||
      html_cache_len = 0;
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  *output = html_cache;
 | 
			
		||||
  *output_len = html_cache_len;
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void pss_buffer_free(struct pss_http *pss) {
 | 
			
		||||
  if (pss->buffer != (char *)index_html && pss->buffer != html_cache) free(pss->buffer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void access_log(struct lws *wsi, const char *path) {
 | 
			
		||||
  char rip[50];
 | 
			
		||||
 | 
			
		||||
  lws_get_peer_simple(lws_get_network_wsi(wsi), rip, sizeof(rip));
 | 
			
		||||
  lwsl_notice("HTTP %s - %s\n", path, rip);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
 | 
			
		||||
  struct pss_http *pss = (struct pss_http *)user;
 | 
			
		||||
  unsigned char buffer[4096 + LWS_PRE], *p, *end;
 | 
			
		||||
  char buf[256];
 | 
			
		||||
  bool done = false;
 | 
			
		||||
 | 
			
		||||
  switch (reason) {
 | 
			
		||||
    case LWS_CALLBACK_HTTP:
 | 
			
		||||
      access_log(wsi, (const char *)in);
 | 
			
		||||
      snprintf(pss->path, sizeof(pss->path), "%s", (const char *)in);
 | 
			
		||||
      switch (check_auth(wsi, pss)) {
 | 
			
		||||
        case AUTH_OK:
 | 
			
		||||
          break;
 | 
			
		||||
        case AUTH_FAIL:
 | 
			
		||||
          return 0;
 | 
			
		||||
        case AUTH_ERROR:
 | 
			
		||||
        default:
 | 
			
		||||
          return 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      p = buffer + LWS_PRE;
 | 
			
		||||
      end = p + sizeof(buffer) - LWS_PRE;
 | 
			
		||||
 | 
			
		||||
      if (strcmp(pss->path, endpoints.token) == 0) {
 | 
			
		||||
        const char *credential = server->credential != NULL ? server->credential : "";
 | 
			
		||||
        size_t n = sprintf(buf, "{\"token\": \"%s\"}", credential);
 | 
			
		||||
        if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
 | 
			
		||||
            lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
 | 
			
		||||
                                         (unsigned char *)"application/json;charset=utf-8", 30, &p, end) ||
 | 
			
		||||
            lws_add_http_header_content_length(wsi, (unsigned long)n, &p, end) ||
 | 
			
		||||
            lws_finalize_http_header(wsi, &p, end) ||
 | 
			
		||||
            lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
 | 
			
		||||
          return 1;
 | 
			
		||||
 | 
			
		||||
        pss->buffer = pss->ptr = strdup(buf);
 | 
			
		||||
        pss->len = n;
 | 
			
		||||
        lws_callback_on_writable(wsi);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // redirects `/base-path` to `/base-path/`
 | 
			
		||||
      if (strcmp(pss->path, endpoints.parent) == 0) {
 | 
			
		||||
        if (lws_add_http_header_status(wsi, HTTP_STATUS_FOUND, &p, end) ||
 | 
			
		||||
            lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)endpoints.index,
 | 
			
		||||
                                         (int)strlen(endpoints.index), &p, end) ||
 | 
			
		||||
            lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
 | 
			
		||||
            lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
 | 
			
		||||
          return 1;
 | 
			
		||||
        goto try_to_reuse;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (strcmp(pss->path, endpoints.index) != 0) {
 | 
			
		||||
        lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
 | 
			
		||||
        goto try_to_reuse;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const char *content_type = "text/html";
 | 
			
		||||
      if (server->index != NULL) {
 | 
			
		||||
        int n = lws_serve_http_file(wsi, server->index, content_type, NULL, 0);
 | 
			
		||||
        if (n < 0 || (n > 0 && lws_http_transaction_completed(wsi))) return 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        char *output = (char *)index_html;
 | 
			
		||||
        size_t output_len = index_html_len;
 | 
			
		||||
        if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
 | 
			
		||||
            lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (const unsigned char *)content_type, 9, &p,
 | 
			
		||||
                                         end))
 | 
			
		||||
          return 1;
 | 
			
		||||
#ifdef LWS_WITH_HTTP_STREAM_COMPRESSION
 | 
			
		||||
        if (!uncompress_html(&output, &output_len)) return 1;
 | 
			
		||||
#else
 | 
			
		||||
        if (accept_gzip(wsi)) {
 | 
			
		||||
          if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING, (unsigned char *)"gzip", 4, &p, end))
 | 
			
		||||
            return 1;
 | 
			
		||||
        } else {
 | 
			
		||||
          if (!uncompress_html(&output, &output_len)) return 1;
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
        if (lws_add_http_header_content_length(wsi, (unsigned long)output_len, &p, end) ||
 | 
			
		||||
            lws_finalize_http_header(wsi, &p, end) ||
 | 
			
		||||
            lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
 | 
			
		||||
          return 1;
 | 
			
		||||
 | 
			
		||||
        pss->buffer = pss->ptr = output;
 | 
			
		||||
        pss->len = output_len;
 | 
			
		||||
        lws_callback_on_writable(wsi);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case LWS_CALLBACK_HTTP_WRITEABLE:
 | 
			
		||||
      if (!pss->buffer || pss->len == 0) {
 | 
			
		||||
        goto try_to_reuse;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      do {
 | 
			
		||||
        int n = sizeof(buffer) - LWS_PRE;
 | 
			
		||||
        int m = lws_get_peer_write_allowance(wsi);
 | 
			
		||||
        if (m == 0) {
 | 
			
		||||
          lws_callback_on_writable(wsi);
 | 
			
		||||
          return 0;
 | 
			
		||||
        } else if (m != -1 && m < n) {
 | 
			
		||||
          n = m;
 | 
			
		||||
        }
 | 
			
		||||
        if (pss->ptr + n > pss->buffer + pss->len) {
 | 
			
		||||
          n = (int)(pss->len - (pss->ptr - pss->buffer));
 | 
			
		||||
          done = true;
 | 
			
		||||
        }
 | 
			
		||||
        memcpy(buffer + LWS_PRE, pss->ptr, n);
 | 
			
		||||
        pss->ptr += n;
 | 
			
		||||
        if (lws_write_http(wsi, buffer + LWS_PRE, (size_t)n) < n) {
 | 
			
		||||
          pss_buffer_free(pss);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
      } while (!lws_send_pipe_choked(wsi) && !done);
 | 
			
		||||
 | 
			
		||||
      if (!done && pss->ptr < pss->buffer + pss->len) {
 | 
			
		||||
        lws_callback_on_writable(wsi);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      pss_buffer_free(pss);
 | 
			
		||||
      goto try_to_reuse;
 | 
			
		||||
 | 
			
		||||
    case LWS_CALLBACK_HTTP_FILE_COMPLETION:
 | 
			
		||||
      goto try_to_reuse;
 | 
			
		||||
#if (defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)) && !defined(LWS_WITH_MBEDTLS)
 | 
			
		||||
    case LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION:
 | 
			
		||||
      if (!len || (SSL_get_verify_result((SSL *)in) != X509_V_OK)) {
 | 
			
		||||
        int err = X509_STORE_CTX_get_error((X509_STORE_CTX *)user);
 | 
			
		||||
        int depth = X509_STORE_CTX_get_error_depth((X509_STORE_CTX *)user);
 | 
			
		||||
        const char *msg = X509_verify_cert_error_string(err);
 | 
			
		||||
        lwsl_err("client certificate verification error: %s (%d), depth: %d\n", msg, err, depth);
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
#endif
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 0;
 | 
			
		||||
 | 
			
		||||
  /* if we're on HTTP1.1 or 2.0, will keep the idle connection alive */
 | 
			
		||||
try_to_reuse:
 | 
			
		||||
  if (lws_http_transaction_completed(wsi)) return -1;
 | 
			
		||||
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										395
									
								
								src/protocol.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										395
									
								
								src/protocol.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,395 @@
 | 
			
		||||
#include <errno.h>
 | 
			
		||||
#include <json.h>
 | 
			
		||||
#include <libwebsockets.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
 | 
			
		||||
#include "pty.h"
 | 
			
		||||
#include "server.h"
 | 
			
		||||
#include "utils.h"
 | 
			
		||||
 | 
			
		||||
// initial message list
 | 
			
		||||
static char initial_cmds[] = {SET_WINDOW_TITLE, SET_PREFERENCES};
 | 
			
		||||
 | 
			
		||||
static int send_initial_message(struct lws *wsi, int index) {
 | 
			
		||||
  unsigned char message[LWS_PRE + 1 + 4096];
 | 
			
		||||
  unsigned char *p = &message[LWS_PRE];
 | 
			
		||||
  char buffer[128];
 | 
			
		||||
  int n = 0;
 | 
			
		||||
 | 
			
		||||
  char cmd = initial_cmds[index];
 | 
			
		||||
  switch (cmd) {
 | 
			
		||||
    case SET_WINDOW_TITLE:
 | 
			
		||||
      gethostname(buffer, sizeof(buffer) - 1);
 | 
			
		||||
      n = sprintf((char *)p, "%c%s (%s)", cmd, server->command, buffer);
 | 
			
		||||
      break;
 | 
			
		||||
    case SET_PREFERENCES:
 | 
			
		||||
      n = sprintf((char *)p, "%c%s", cmd, server->prefs_json);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static json_object *parse_window_size(const char *buf, size_t len, uint16_t *cols, uint16_t *rows) {
 | 
			
		||||
  json_tokener *tok = json_tokener_new();
 | 
			
		||||
  json_object *obj = json_tokener_parse_ex(tok, buf, len);
 | 
			
		||||
  struct json_object *o = NULL;
 | 
			
		||||
 | 
			
		||||
  if (json_object_object_get_ex(obj, "columns", &o)) *cols = (uint16_t)json_object_get_int(o);
 | 
			
		||||
  if (json_object_object_get_ex(obj, "rows", &o)) *rows = (uint16_t)json_object_get_int(o);
 | 
			
		||||
 | 
			
		||||
  json_tokener_free(tok);
 | 
			
		||||
  return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool check_host_origin(struct lws *wsi) {
 | 
			
		||||
  char buf[256];
 | 
			
		||||
  memset(buf, 0, sizeof(buf));
 | 
			
		||||
  int len = lws_hdr_copy(wsi, buf, (int)sizeof(buf), WSI_TOKEN_ORIGIN);
 | 
			
		||||
  if (len <= 0) return false;
 | 
			
		||||
 | 
			
		||||
  const char *prot, *address, *path;
 | 
			
		||||
  int port;
 | 
			
		||||
  if (lws_parse_uri(buf, &prot, &address, &port, &path)) return false;
 | 
			
		||||
  if (port == 80 || port == 443) {
 | 
			
		||||
    sprintf(buf, "%s", address);
 | 
			
		||||
  } else {
 | 
			
		||||
    sprintf(buf, "%s:%d", address, port);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  char host_buf[256];
 | 
			
		||||
  memset(host_buf, 0, sizeof(host_buf));
 | 
			
		||||
  len = lws_hdr_copy(wsi, host_buf, (int)sizeof(host_buf), WSI_TOKEN_HOST);
 | 
			
		||||
 | 
			
		||||
  return len > 0 && strcasecmp(buf, host_buf) == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static pty_ctx_t *pty_ctx_init(struct pss_tty *pss) {
 | 
			
		||||
  pty_ctx_t *ctx = xmalloc(sizeof(pty_ctx_t));
 | 
			
		||||
  ctx->pss = pss;
 | 
			
		||||
  ctx->ws_closed = false;
 | 
			
		||||
  return ctx;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void pty_ctx_free(pty_ctx_t *ctx) { free(ctx); }
 | 
			
		||||
 | 
			
		||||
static void process_read_cb(pty_process *process, pty_buf_t *buf, bool eof) {
 | 
			
		||||
  pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
 | 
			
		||||
  if (ctx->ws_closed) {
 | 
			
		||||
    pty_buf_free(buf);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (eof && !process_running(process))
 | 
			
		||||
    ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
 | 
			
		||||
  else
 | 
			
		||||
    ctx->pss->pty_buf = buf;
 | 
			
		||||
  lws_callback_on_writable(ctx->pss->wsi);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void process_exit_cb(pty_process *process) {
 | 
			
		||||
  pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
 | 
			
		||||
  if (ctx->ws_closed) {
 | 
			
		||||
    lwsl_notice("process killed with signal %d, pid: %d\n", process->exit_signal, process->pid);
 | 
			
		||||
    goto done;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  lwsl_notice("process exited with code %d, pid: %d\n", process->exit_code, process->pid);
 | 
			
		||||
  ctx->pss->process = NULL;
 | 
			
		||||
  ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
 | 
			
		||||
  lws_callback_on_writable(ctx->pss->wsi);
 | 
			
		||||
 | 
			
		||||
done:
 | 
			
		||||
  pty_ctx_free(ctx);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static char **build_args(struct pss_tty *pss) {
 | 
			
		||||
  int i, n = 0;
 | 
			
		||||
  char **argv = xmalloc((server->argc + pss->argc + 1) * sizeof(char *));
 | 
			
		||||
 | 
			
		||||
  for (i = 0; i < server->argc; i++) {
 | 
			
		||||
    argv[n++] = server->argv[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (i = 0; i < pss->argc; i++) {
 | 
			
		||||
    argv[n++] = pss->args[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  argv[n] = NULL;
 | 
			
		||||
 | 
			
		||||
  return argv;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static char **build_env(struct pss_tty *pss) {
 | 
			
		||||
  int i = 0, n = 2;
 | 
			
		||||
  char **envp = xmalloc(n * sizeof(char *));
 | 
			
		||||
 | 
			
		||||
  // TERM
 | 
			
		||||
  envp[i] = xmalloc(36);
 | 
			
		||||
  snprintf(envp[i], 36, "TERM=%s", server->terminal_type);
 | 
			
		||||
  i++;
 | 
			
		||||
 | 
			
		||||
  // TTYD_USER
 | 
			
		||||
  if (strlen(pss->user) > 0) {
 | 
			
		||||
    envp = xrealloc(envp, (++n) * sizeof(char *));
 | 
			
		||||
    envp[i] = xmalloc(40);
 | 
			
		||||
    snprintf(envp[i], 40, "TTYD_USER=%s", pss->user);
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  envp[i] = NULL;
 | 
			
		||||
 | 
			
		||||
  return envp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool spawn_process(struct pss_tty *pss, uint16_t columns, uint16_t rows) {
 | 
			
		||||
  pty_process *process = process_init((void *)pty_ctx_init(pss), server->loop, build_args(pss), build_env(pss));
 | 
			
		||||
  if (server->cwd != NULL) process->cwd = strdup(server->cwd);
 | 
			
		||||
  if (columns > 0) process->columns = columns;
 | 
			
		||||
  if (rows > 0) process->rows = rows;
 | 
			
		||||
  if (pty_spawn(process, process_read_cb, process_exit_cb) != 0) {
 | 
			
		||||
    lwsl_err("pty_spawn: %d (%s)\n", errno, strerror(errno));
 | 
			
		||||
    process_free(process);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  lwsl_notice("started process, pid: %d\n", process->pid);
 | 
			
		||||
  pss->process = process;
 | 
			
		||||
  lws_callback_on_writable(pss->wsi);
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void wsi_output(struct lws *wsi, pty_buf_t *buf) {
 | 
			
		||||
  if (buf == NULL) return;
 | 
			
		||||
  char *message = xmalloc(LWS_PRE + 1 + buf->len);
 | 
			
		||||
  char *ptr = message + LWS_PRE;
 | 
			
		||||
 | 
			
		||||
  *ptr = OUTPUT;
 | 
			
		||||
  memcpy(ptr + 1, buf->base, buf->len);
 | 
			
		||||
  size_t n = buf->len + 1;
 | 
			
		||||
 | 
			
		||||
  if (lws_write(wsi, (unsigned char *)ptr, n, LWS_WRITE_BINARY) < n) {
 | 
			
		||||
    lwsl_err("write OUTPUT to WS\n");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  free(message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool check_auth(struct lws *wsi, struct pss_tty *pss) {
 | 
			
		||||
  if (server->auth_header != NULL) {
 | 
			
		||||
    return lws_hdr_custom_copy(wsi, pss->user, sizeof(pss->user), server->auth_header, strlen(server->auth_header)) > 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (server->credential != NULL) {
 | 
			
		||||
    char buf[256];
 | 
			
		||||
    size_t n = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
 | 
			
		||||
    return n >= 7 && strstr(buf, "Basic ") && !strcmp(buf + 6, server->credential);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
 | 
			
		||||
  struct pss_tty *pss = (struct pss_tty *)user;
 | 
			
		||||
  char buf[256];
 | 
			
		||||
  size_t n = 0;
 | 
			
		||||
 | 
			
		||||
  switch (reason) {
 | 
			
		||||
    case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
 | 
			
		||||
      if (server->once && server->client_count > 0) {
 | 
			
		||||
        lwsl_warn("refuse to serve WS client due to the --once option.\n");
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
      if (server->max_clients > 0 && server->client_count == server->max_clients) {
 | 
			
		||||
        lwsl_warn("refuse to serve WS client due to the --max-clients option.\n");
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
      if (!check_auth(wsi, pss)) return 1;
 | 
			
		||||
 | 
			
		||||
      n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_GET_URI);
 | 
			
		||||
#if defined(LWS_ROLE_H2)
 | 
			
		||||
      if (n <= 0) n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_HTTP_COLON_PATH);
 | 
			
		||||
#endif
 | 
			
		||||
      if (strncmp(pss->path, endpoints.ws, n) != 0) {
 | 
			
		||||
        lwsl_warn("refuse to serve WS client for illegal ws path: %s\n", pss->path);
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (server->check_origin && !check_host_origin(wsi)) {
 | 
			
		||||
        lwsl_warn(
 | 
			
		||||
            "refuse to serve WS client from different origin due to the "
 | 
			
		||||
            "--check-origin option.\n");
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case LWS_CALLBACK_ESTABLISHED:
 | 
			
		||||
      pss->initialized = false;
 | 
			
		||||
      pss->authenticated = false;
 | 
			
		||||
      pss->wsi = wsi;
 | 
			
		||||
      pss->lws_close_status = LWS_CLOSE_STATUS_NOSTATUS;
 | 
			
		||||
 | 
			
		||||
      if (server->url_arg) {
 | 
			
		||||
        while (lws_hdr_copy_fragment(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_URI_ARGS, n++) > 0) {
 | 
			
		||||
          if (strncmp(buf, "arg=", 4) == 0) {
 | 
			
		||||
            pss->args = xrealloc(pss->args, (pss->argc + 1) * sizeof(char *));
 | 
			
		||||
            pss->args[pss->argc] = strdup(&buf[4]);
 | 
			
		||||
            pss->argc++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      server->client_count++;
 | 
			
		||||
 | 
			
		||||
      lws_get_peer_simple(lws_get_network_wsi(wsi), pss->address, sizeof(pss->address));
 | 
			
		||||
      lwsl_notice("WS   %s - %s, clients: %d\n", pss->path, pss->address, server->client_count);
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case LWS_CALLBACK_SERVER_WRITEABLE:
 | 
			
		||||
      if (!pss->initialized) {
 | 
			
		||||
        if (pss->initial_cmd_index == sizeof(initial_cmds)) {
 | 
			
		||||
          pss->initialized = true;
 | 
			
		||||
          pty_resume(pss->process);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        if (send_initial_message(wsi, pss->initial_cmd_index) < 0) {
 | 
			
		||||
          lwsl_err("failed to send initial message, index: %d\n", pss->initial_cmd_index);
 | 
			
		||||
          lws_close_reason(wsi, LWS_CLOSE_STATUS_UNEXPECTED_CONDITION, NULL, 0);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        pss->initial_cmd_index++;
 | 
			
		||||
        lws_callback_on_writable(wsi);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (pss->lws_close_status > LWS_CLOSE_STATUS_NOSTATUS) {
 | 
			
		||||
        lws_close_reason(wsi, pss->lws_close_status, NULL, 0);
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (pss->pty_buf != NULL) {
 | 
			
		||||
        wsi_output(wsi, pss->pty_buf);
 | 
			
		||||
        pty_buf_free(pss->pty_buf);
 | 
			
		||||
        pss->pty_buf = NULL;
 | 
			
		||||
        pty_resume(pss->process);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case LWS_CALLBACK_RECEIVE:
 | 
			
		||||
      if (pss->buffer == NULL) {
 | 
			
		||||
        pss->buffer = xmalloc(len);
 | 
			
		||||
        pss->len = len;
 | 
			
		||||
        memcpy(pss->buffer, in, len);
 | 
			
		||||
      } else {
 | 
			
		||||
        pss->buffer = xrealloc(pss->buffer, pss->len + len);
 | 
			
		||||
        memcpy(pss->buffer + pss->len, in, len);
 | 
			
		||||
        pss->len += len;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const char command = pss->buffer[0];
 | 
			
		||||
 | 
			
		||||
      // check auth
 | 
			
		||||
      if (server->credential != NULL && !pss->authenticated && command != JSON_DATA) {
 | 
			
		||||
        lwsl_warn("WS client not authenticated\n");
 | 
			
		||||
        return 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // check if there are more fragmented messages
 | 
			
		||||
      if (lws_remaining_packet_payload(wsi) > 0 || !lws_is_final_fragment(wsi)) {
 | 
			
		||||
        return 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      switch (command) {
 | 
			
		||||
        case INPUT:
 | 
			
		||||
          if (!server->writable) break;
 | 
			
		||||
          int err = pty_write(pss->process, pty_buf_init(pss->buffer + 1, pss->len - 1));
 | 
			
		||||
          if (err) {
 | 
			
		||||
            lwsl_err("uv_write: %s (%s)\n", uv_err_name(err), uv_strerror(err));
 | 
			
		||||
            return -1;
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case RESIZE_TERMINAL:
 | 
			
		||||
          if (pss->process == NULL) break;
 | 
			
		||||
          json_object_put(
 | 
			
		||||
              parse_window_size(pss->buffer + 1, pss->len - 1, &pss->process->columns, &pss->process->rows));
 | 
			
		||||
          pty_resize(pss->process);
 | 
			
		||||
          break;
 | 
			
		||||
        case PAUSE:
 | 
			
		||||
          pty_pause(pss->process);
 | 
			
		||||
          break;
 | 
			
		||||
        case RESUME:
 | 
			
		||||
          pty_resume(pss->process);
 | 
			
		||||
          break;
 | 
			
		||||
        case JSON_DATA:
 | 
			
		||||
          if (pss->process != NULL) break;
 | 
			
		||||
          uint16_t columns = 0;
 | 
			
		||||
          uint16_t rows = 0;
 | 
			
		||||
          json_object *obj = parse_window_size(pss->buffer, pss->len, &columns, &rows);
 | 
			
		||||
          if (server->credential != NULL) {
 | 
			
		||||
            struct json_object *o = NULL;
 | 
			
		||||
            if (json_object_object_get_ex(obj, "AuthToken", &o)) {
 | 
			
		||||
              const char *token = json_object_get_string(o);
 | 
			
		||||
              if (token != NULL && !strcmp(token, server->credential))
 | 
			
		||||
                pss->authenticated = true;
 | 
			
		||||
              else
 | 
			
		||||
                lwsl_warn("WS authentication failed with token: %s\n", token);
 | 
			
		||||
            }
 | 
			
		||||
            if (!pss->authenticated) {
 | 
			
		||||
              json_object_put(obj);
 | 
			
		||||
              lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, NULL, 0);
 | 
			
		||||
              return -1;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          json_object_put(obj);
 | 
			
		||||
          if (!spawn_process(pss, columns, rows)) return 1;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          lwsl_warn("ignored unknown message type: %c\n", command);
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (pss->buffer != NULL) {
 | 
			
		||||
        free(pss->buffer);
 | 
			
		||||
        pss->buffer = NULL;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case LWS_CALLBACK_CLOSED:
 | 
			
		||||
      if (pss->wsi == NULL) break;
 | 
			
		||||
 | 
			
		||||
      server->client_count--;
 | 
			
		||||
      lwsl_notice("WS closed from %s, clients: %d\n", pss->address, server->client_count);
 | 
			
		||||
      if (pss->buffer != NULL) free(pss->buffer);
 | 
			
		||||
      if (pss->pty_buf != NULL) pty_buf_free(pss->pty_buf);
 | 
			
		||||
      for (int i = 0; i < pss->argc; i++) {
 | 
			
		||||
        free(pss->args[i]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (pss->process != NULL) {
 | 
			
		||||
        ((pty_ctx_t *)pss->process->ctx)->ws_closed = true;
 | 
			
		||||
        if (process_running(pss->process)) {
 | 
			
		||||
          pty_pause(pss->process);
 | 
			
		||||
          lwsl_notice("killing process, pid: %d\n", pss->process->pid);
 | 
			
		||||
          pty_kill(pss->process, server->sig_code);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if ((server->once || server->exit_no_conn) && server->client_count == 0) {
 | 
			
		||||
        lwsl_notice("exiting due to the --once/--exit-no-conn option.\n");
 | 
			
		||||
        force_exit = true;
 | 
			
		||||
        lws_cancel_service(context);
 | 
			
		||||
        exit(0);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										485
									
								
								src/pty.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										485
									
								
								src/pty.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,485 @@
 | 
			
		||||
#include <errno.h>
 | 
			
		||||
#include <fcntl.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
 | 
			
		||||
#ifndef _WIN32
 | 
			
		||||
#include <sys/ioctl.h>
 | 
			
		||||
#include <sys/wait.h>
 | 
			
		||||
 | 
			
		||||
#if defined(__OpenBSD__) || defined(__APPLE__)
 | 
			
		||||
#include <util.h>
 | 
			
		||||
#elif defined(__FreeBSD__)
 | 
			
		||||
#include <libutil.h>
 | 
			
		||||
#else
 | 
			
		||||
#include <pty.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(__APPLE__)
 | 
			
		||||
#include <crt_externs.h>
 | 
			
		||||
#define environ (*_NSGetEnviron())
 | 
			
		||||
#else
 | 
			
		||||
extern char **environ;
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "pty.h"
 | 
			
		||||
#include "utils.h"
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
HRESULT (WINAPI *pCreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, HPCON *);
 | 
			
		||||
HRESULT (WINAPI *pResizePseudoConsole)(HPCON, COORD);
 | 
			
		||||
void (WINAPI *pClosePseudoConsole)(HPCON);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
static void alloc_cb(uv_handle_t *unused, size_t suggested_size, uv_buf_t *buf) {
 | 
			
		||||
  buf->base = xmalloc(suggested_size);
 | 
			
		||||
  buf->len = suggested_size;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void close_cb(uv_handle_t *handle) { free(handle); }
 | 
			
		||||
 | 
			
		||||
static void async_free_cb(uv_handle_t *handle) {
 | 
			
		||||
  free((uv_async_t *) handle -> data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pty_buf_t *pty_buf_init(char *base, size_t len) {
 | 
			
		||||
  pty_buf_t *buf = xmalloc(sizeof(pty_buf_t));
 | 
			
		||||
  buf->base = xmalloc(len);
 | 
			
		||||
  memcpy(buf->base, base, len);
 | 
			
		||||
  buf->len = len;
 | 
			
		||||
  return buf;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void pty_buf_free(pty_buf_t *buf) {
 | 
			
		||||
  if (buf == NULL) return;
 | 
			
		||||
  if (buf->base != NULL) free(buf->base);
 | 
			
		||||
  free(buf);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void read_cb(uv_stream_t *stream, ssize_t n, const uv_buf_t *buf) {
 | 
			
		||||
  uv_read_stop(stream);
 | 
			
		||||
  pty_process *process = (pty_process *) stream->data;
 | 
			
		||||
  if (n <= 0) {
 | 
			
		||||
    if (n == UV_ENOBUFS || n == 0) return;
 | 
			
		||||
    process->read_cb(process, NULL, true);
 | 
			
		||||
    goto done;
 | 
			
		||||
  }
 | 
			
		||||
  process->read_cb(process, pty_buf_init(buf->base, (size_t) n), false);
 | 
			
		||||
 | 
			
		||||
done:
 | 
			
		||||
  free(buf->base);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void write_cb(uv_write_t *req, int unused) {
 | 
			
		||||
  pty_buf_t *buf = (pty_buf_t *) req->data;
 | 
			
		||||
  pty_buf_free(buf);
 | 
			
		||||
  free(req);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]) {
 | 
			
		||||
  pty_process *process = xmalloc(sizeof(pty_process));
 | 
			
		||||
  memset(process, 0, sizeof(pty_process));
 | 
			
		||||
  process->ctx = ctx;
 | 
			
		||||
  process->loop = loop;
 | 
			
		||||
  process->argv = argv;
 | 
			
		||||
  process->envp = envp;
 | 
			
		||||
  process->columns = 80;
 | 
			
		||||
  process->rows = 24;
 | 
			
		||||
  process->exit_code = -1;
 | 
			
		||||
  return process;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool process_running(pty_process *process) {
 | 
			
		||||
  return process != NULL && process->pid > 0 && uv_kill(process->pid, 0) == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void process_free(pty_process *process) {
 | 
			
		||||
  if (process == NULL) return;
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
  if (process->si.lpAttributeList != NULL) {
 | 
			
		||||
    DeleteProcThreadAttributeList(process->si.lpAttributeList);
 | 
			
		||||
    free(process->si.lpAttributeList);
 | 
			
		||||
  }
 | 
			
		||||
  if (process->pty != NULL) pClosePseudoConsole(process->pty);
 | 
			
		||||
  if (process->handle != NULL) CloseHandle(process->handle);
 | 
			
		||||
#else
 | 
			
		||||
  close(process->pty);
 | 
			
		||||
  uv_thread_join(&process->tid);
 | 
			
		||||
#endif
 | 
			
		||||
  if (process->in != NULL) uv_close((uv_handle_t *) process->in, close_cb);
 | 
			
		||||
  if (process->out != NULL) uv_close((uv_handle_t *) process->out, close_cb);
 | 
			
		||||
  if (process->argv != NULL) free(process->argv);
 | 
			
		||||
  if (process->cwd != NULL) free(process->cwd);
 | 
			
		||||
  char **p = process->envp;
 | 
			
		||||
  for (; *p; p++) free(*p);
 | 
			
		||||
  free(process->envp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void pty_pause(pty_process *process) {
 | 
			
		||||
  if (process == NULL) return;
 | 
			
		||||
  if (process->paused) return;
 | 
			
		||||
  uv_read_stop((uv_stream_t *) process->out);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void pty_resume(pty_process *process) {
 | 
			
		||||
  if (process == NULL) return;
 | 
			
		||||
  if (!process->paused) return;
 | 
			
		||||
  process->out->data = process;
 | 
			
		||||
  uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int pty_write(pty_process *process, pty_buf_t *buf) {
 | 
			
		||||
  if (process == NULL) {
 | 
			
		||||
    pty_buf_free(buf);
 | 
			
		||||
    return UV_ESRCH;
 | 
			
		||||
  }
 | 
			
		||||
  uv_buf_t b = uv_buf_init(buf->base, buf->len);
 | 
			
		||||
  uv_write_t *req = xmalloc(sizeof(uv_write_t));
 | 
			
		||||
  req->data = buf;
 | 
			
		||||
  return uv_write(req, (uv_stream_t *) process->in, &b, 1, write_cb);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool pty_resize(pty_process *process) {
 | 
			
		||||
  if (process == NULL) return false;
 | 
			
		||||
  if (process->columns <= 0 || process->rows <= 0) return false;
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
  COORD size = {(int16_t) process->columns, (int16_t) process->rows};
 | 
			
		||||
  return pResizePseudoConsole(process->pty, size) == S_OK;
 | 
			
		||||
#else
 | 
			
		||||
  struct winsize size = {process->rows, process->columns, 0, 0};
 | 
			
		||||
  return ioctl(process->pty, TIOCSWINSZ, &size) == 0;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool pty_kill(pty_process *process, int sig) {
 | 
			
		||||
  if (process == NULL) return false;
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
  return TerminateProcess(process->handle, 1) != 0;
 | 
			
		||||
#else
 | 
			
		||||
  return uv_kill(-process->pid, sig) == 0;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
bool conpty_init() {
 | 
			
		||||
  uv_lib_t kernel;
 | 
			
		||||
  if (uv_dlopen("kernel32.dll", &kernel)) {
 | 
			
		||||
    uv_dlclose(&kernel);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  static struct {
 | 
			
		||||
    char *name;
 | 
			
		||||
    FARPROC *ptr;
 | 
			
		||||
  } conpty_entry[] = {{"CreatePseudoConsole", (FARPROC *) &pCreatePseudoConsole},
 | 
			
		||||
                      {"ResizePseudoConsole", (FARPROC *) &pResizePseudoConsole},
 | 
			
		||||
                      {"ClosePseudoConsole", (FARPROC *) &pClosePseudoConsole},
 | 
			
		||||
                      {NULL, NULL}};
 | 
			
		||||
  for (int i = 0; conpty_entry[i].name != NULL && conpty_entry[i].ptr != NULL; i++) {
 | 
			
		||||
    if (uv_dlsym(&kernel, conpty_entry[i].name, (void **) conpty_entry[i].ptr)) {
 | 
			
		||||
      uv_dlclose(&kernel);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static WCHAR *to_utf16(char *str) {
 | 
			
		||||
  int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
 | 
			
		||||
  if (len <= 0) return NULL;
 | 
			
		||||
  WCHAR *wstr = xmalloc((len + 1) * sizeof(WCHAR));
 | 
			
		||||
  if (len != MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len)) {
 | 
			
		||||
    free(wstr);
 | 
			
		||||
    return NULL;
 | 
			
		||||
  }
 | 
			
		||||
  wstr[len] = L'\0';
 | 
			
		||||
  return wstr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// convert argv to cmdline for CreateProcessW
 | 
			
		||||
static WCHAR *join_args(char **argv) {
 | 
			
		||||
  char args[256] = {0};
 | 
			
		||||
  char **ptr = argv;
 | 
			
		||||
  for (; *ptr; ptr++) {
 | 
			
		||||
    char *quoted = (char *) quote_arg(*ptr);
 | 
			
		||||
    size_t arg_len = strlen(args) + 1;
 | 
			
		||||
    size_t quoted_len = strlen(quoted);
 | 
			
		||||
    if (arg_len == 1) memset(args, 0, 2);
 | 
			
		||||
    if (arg_len != 1) strcat(args, " ");
 | 
			
		||||
    strncat(args, quoted, quoted_len);
 | 
			
		||||
    if (quoted != *ptr) free(quoted);
 | 
			
		||||
  }
 | 
			
		||||
  if (args[255] != '\0') args[255] = '\0';  // truncate
 | 
			
		||||
  return to_utf16(args);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool conpty_setup(HPCON *hnd, COORD size, STARTUPINFOEXW *si_ex, char **in_name, char **out_name) {
 | 
			
		||||
  static int count = 0;
 | 
			
		||||
  char buf[256];
 | 
			
		||||
  HPCON pty = INVALID_HANDLE_VALUE;
 | 
			
		||||
  SECURITY_ATTRIBUTES sa = {0};
 | 
			
		||||
  HANDLE in_pipe = INVALID_HANDLE_VALUE;
 | 
			
		||||
  HANDLE out_pipe = INVALID_HANDLE_VALUE;
 | 
			
		||||
  const DWORD open_mode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE;
 | 
			
		||||
  const DWORD pipe_mode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT;
 | 
			
		||||
  DWORD pid = GetCurrentProcessId();
 | 
			
		||||
  bool ret = false;
 | 
			
		||||
 | 
			
		||||
  sa.nLength = sizeof(sa);
 | 
			
		||||
  snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-in-%d-%d", pid, count);
 | 
			
		||||
  *in_name = strdup(buf);
 | 
			
		||||
  snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-out-%d-%d", pid, count);
 | 
			
		||||
  *out_name = strdup(buf);
 | 
			
		||||
  in_pipe = CreateNamedPipeA(*in_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa);
 | 
			
		||||
  out_pipe = CreateNamedPipeA(*out_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa);
 | 
			
		||||
  if (in_pipe == INVALID_HANDLE_VALUE || out_pipe == INVALID_HANDLE_VALUE) {
 | 
			
		||||
    print_error("CreateNamedPipeA");
 | 
			
		||||
    goto failed;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  HRESULT hr = pCreatePseudoConsole(size, in_pipe, out_pipe, 0, &pty);
 | 
			
		||||
  if (FAILED(hr)) {
 | 
			
		||||
    print_error("CreatePseudoConsole");
 | 
			
		||||
    goto failed;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  si_ex->StartupInfo.cb = sizeof(STARTUPINFOEXW);
 | 
			
		||||
  si_ex->StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
 | 
			
		||||
  si_ex->StartupInfo.hStdError = NULL;
 | 
			
		||||
  si_ex->StartupInfo.hStdInput = NULL;
 | 
			
		||||
  si_ex->StartupInfo.hStdOutput = NULL;
 | 
			
		||||
  size_t bytes_required;
 | 
			
		||||
  InitializeProcThreadAttributeList(NULL, 1, 0, &bytes_required);
 | 
			
		||||
  si_ex->lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST) xmalloc(bytes_required);
 | 
			
		||||
  if (!InitializeProcThreadAttributeList(si_ex->lpAttributeList, 1, 0, &bytes_required)) {
 | 
			
		||||
    print_error("InitializeProcThreadAttributeList");
 | 
			
		||||
    goto failed;
 | 
			
		||||
  }
 | 
			
		||||
  if (!UpdateProcThreadAttribute(si_ex->lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pty, sizeof(HPCON),
 | 
			
		||||
                                 NULL, NULL)) {
 | 
			
		||||
    print_error("UpdateProcThreadAttribute");
 | 
			
		||||
    goto failed;
 | 
			
		||||
  }
 | 
			
		||||
  count++;
 | 
			
		||||
  *hnd = pty;
 | 
			
		||||
  ret = true;
 | 
			
		||||
  goto done;
 | 
			
		||||
 | 
			
		||||
failed:
 | 
			
		||||
  ret = false;
 | 
			
		||||
  free(*in_name);
 | 
			
		||||
  *in_name = NULL;
 | 
			
		||||
  free(*out_name);
 | 
			
		||||
  *out_name = NULL;
 | 
			
		||||
done:
 | 
			
		||||
  if (in_pipe != INVALID_HANDLE_VALUE) CloseHandle(in_pipe);
 | 
			
		||||
  if (out_pipe != INVALID_HANDLE_VALUE) CloseHandle(out_pipe);
 | 
			
		||||
  return ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void connect_cb(uv_connect_t *req, int status) { free(req); }
 | 
			
		||||
 | 
			
		||||
static void CALLBACK conpty_exit(void *context, BOOLEAN unused) {
 | 
			
		||||
  pty_process *process = (pty_process *) context;
 | 
			
		||||
  uv_async_send(&process->async);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void async_cb(uv_async_t *async) {
 | 
			
		||||
  pty_process *process = (pty_process *) async->data;
 | 
			
		||||
  UnregisterWait(process->wait);
 | 
			
		||||
 | 
			
		||||
  DWORD exit_code;
 | 
			
		||||
  GetExitCodeProcess(process->handle, &exit_code);
 | 
			
		||||
  process->exit_code = (int) exit_code;
 | 
			
		||||
  process->exit_signal = 1;
 | 
			
		||||
  process->exit_cb(process);
 | 
			
		||||
 | 
			
		||||
  uv_close((uv_handle_t *) async, async_free_cb);
 | 
			
		||||
  process_free(process);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
 | 
			
		||||
  char *in_name = NULL;
 | 
			
		||||
  char *out_name = NULL;
 | 
			
		||||
  DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT;
 | 
			
		||||
  COORD size = {(int16_t) process->columns, (int16_t) process->rows};
 | 
			
		||||
 | 
			
		||||
  if (!conpty_setup(&process->pty, size, &process->si, &in_name, &out_name)) return 1;
 | 
			
		||||
 | 
			
		||||
  SetConsoleCtrlHandler(NULL, FALSE);
 | 
			
		||||
 | 
			
		||||
  int status = 1;
 | 
			
		||||
  process->in = xmalloc(sizeof(uv_pipe_t));
 | 
			
		||||
  process->out = xmalloc(sizeof(uv_pipe_t));
 | 
			
		||||
  uv_pipe_init(process->loop, process->in, 0);
 | 
			
		||||
  uv_pipe_init(process->loop, process->out, 0);
 | 
			
		||||
 | 
			
		||||
  uv_connect_t *in_req = xmalloc(sizeof(uv_connect_t));
 | 
			
		||||
  uv_connect_t *out_req = xmalloc(sizeof(uv_connect_t));
 | 
			
		||||
  uv_pipe_connect(in_req, process->in, in_name, connect_cb);
 | 
			
		||||
  uv_pipe_connect(out_req, process->out, out_name, connect_cb);
 | 
			
		||||
 | 
			
		||||
  PROCESS_INFORMATION pi = {0};
 | 
			
		||||
  WCHAR *cmdline, *cwd;
 | 
			
		||||
  cmdline = join_args(process->argv);
 | 
			
		||||
  if (cmdline == NULL) goto cleanup;
 | 
			
		||||
  if (process->envp != NULL) {
 | 
			
		||||
    char **p = process->envp;
 | 
			
		||||
    for (; *p; p++) {
 | 
			
		||||
      WCHAR *env = to_utf16(*p);
 | 
			
		||||
      if (env == NULL) goto cleanup;
 | 
			
		||||
      _wputenv(env);
 | 
			
		||||
      free(env);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (process->cwd != NULL) {
 | 
			
		||||
    cwd = to_utf16(process->cwd);
 | 
			
		||||
    if (cwd == NULL) goto cleanup;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, flags, NULL, cwd, &process->si.StartupInfo, &pi)) {
 | 
			
		||||
    print_error("CreateProcessW");
 | 
			
		||||
    DWORD exitCode = 0;
 | 
			
		||||
    if (GetExitCodeProcess(pi.hProcess, &exitCode)) printf("== exit code: %d\n", exitCode);
 | 
			
		||||
    goto cleanup;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  process->pid = pi.dwProcessId;
 | 
			
		||||
  process->handle = pi.hProcess;
 | 
			
		||||
  process->paused = true;
 | 
			
		||||
  process->read_cb = read_cb;
 | 
			
		||||
  process->exit_cb = exit_cb;
 | 
			
		||||
  process->async.data = process;
 | 
			
		||||
  uv_async_init(process->loop, &process->async, async_cb);
 | 
			
		||||
 | 
			
		||||
  if (!RegisterWaitForSingleObject(&process->wait, pi.hProcess, conpty_exit, process, INFINITE, WT_EXECUTEONLYONCE)) {
 | 
			
		||||
    print_error("RegisterWaitForSingleObject");
 | 
			
		||||
    goto cleanup;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  status = 0;
 | 
			
		||||
 | 
			
		||||
cleanup:
 | 
			
		||||
  if (in_name != NULL) free(in_name);
 | 
			
		||||
  if (out_name != NULL) free(out_name);
 | 
			
		||||
  if (cmdline != NULL) free(cmdline);
 | 
			
		||||
  if (cwd != NULL) free(cwd);
 | 
			
		||||
  return status;
 | 
			
		||||
}
 | 
			
		||||
#else
 | 
			
		||||
static bool fd_set_cloexec(const int fd) {
 | 
			
		||||
  int flags = fcntl(fd, F_GETFD);
 | 
			
		||||
  if (flags < 0) return false;
 | 
			
		||||
  return (flags & FD_CLOEXEC) == 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool fd_duplicate(int fd, uv_pipe_t *pipe) {
 | 
			
		||||
  int fd_dup = dup(fd);
 | 
			
		||||
  if (fd_dup < 0) return false;
 | 
			
		||||
 | 
			
		||||
  if (!fd_set_cloexec(fd_dup)) return false;
 | 
			
		||||
 | 
			
		||||
  int status = uv_pipe_open(pipe, fd_dup);
 | 
			
		||||
  if (status) close(fd_dup);
 | 
			
		||||
  return status == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void wait_cb(void *arg) {
 | 
			
		||||
  pty_process *process = (pty_process *) arg;
 | 
			
		||||
 | 
			
		||||
  pid_t pid;
 | 
			
		||||
  int stat;
 | 
			
		||||
  do
 | 
			
		||||
    pid = waitpid(process->pid, &stat, 0);
 | 
			
		||||
  while (pid != process->pid && errno == EINTR);
 | 
			
		||||
 | 
			
		||||
  if (WIFEXITED(stat)) {
 | 
			
		||||
    process->exit_code = WEXITSTATUS(stat);
 | 
			
		||||
  }
 | 
			
		||||
  if (WIFSIGNALED(stat)) {
 | 
			
		||||
    int sig = WTERMSIG(stat);
 | 
			
		||||
    process->exit_code = 128 + sig;
 | 
			
		||||
    process->exit_signal = sig;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uv_async_send(&process->async);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void async_cb(uv_async_t *async) {
 | 
			
		||||
  pty_process *process = (pty_process *) async->data;
 | 
			
		||||
  process->exit_cb(process);
 | 
			
		||||
 | 
			
		||||
  uv_close((uv_handle_t *) async, async_free_cb);
 | 
			
		||||
  process_free(process);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
 | 
			
		||||
  int status = 0;
 | 
			
		||||
 | 
			
		||||
  uv_disable_stdio_inheritance();
 | 
			
		||||
 | 
			
		||||
  int master, pid;
 | 
			
		||||
  struct winsize size = {process->rows, process->columns, 0, 0};
 | 
			
		||||
  pid = forkpty(&master, NULL, NULL, &size);
 | 
			
		||||
  if (pid < 0) {
 | 
			
		||||
    status = -errno;
 | 
			
		||||
    return status;
 | 
			
		||||
  } else if (pid == 0) {
 | 
			
		||||
    setsid();
 | 
			
		||||
    if (process->cwd != NULL) chdir(process->cwd);
 | 
			
		||||
    if (process->envp != NULL) {
 | 
			
		||||
      char **p = process->envp;
 | 
			
		||||
      for (; *p; p++) putenv(*p);
 | 
			
		||||
    }
 | 
			
		||||
    int ret = execvp(process->argv[0], process->argv);
 | 
			
		||||
    if (ret < 0) {
 | 
			
		||||
      perror("execvp failed\n");
 | 
			
		||||
      _exit(-errno);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int flags = fcntl(master, F_GETFL);
 | 
			
		||||
  if (flags == -1) {
 | 
			
		||||
    status = -errno;
 | 
			
		||||
    goto error;
 | 
			
		||||
  }
 | 
			
		||||
  if (fcntl(master, F_SETFL, flags | O_NONBLOCK) == -1) {
 | 
			
		||||
    status = -errno;
 | 
			
		||||
    goto error;
 | 
			
		||||
  }
 | 
			
		||||
  if (!fd_set_cloexec(master)) {
 | 
			
		||||
    status = -errno;
 | 
			
		||||
    goto error;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  process->in = xmalloc(sizeof(uv_pipe_t));
 | 
			
		||||
  process->out = xmalloc(sizeof(uv_pipe_t));
 | 
			
		||||
  uv_pipe_init(process->loop, process->in, 0);
 | 
			
		||||
  uv_pipe_init(process->loop, process->out, 0);
 | 
			
		||||
 | 
			
		||||
  if (!fd_duplicate(master, process->in) || !fd_duplicate(master, process->out)) {
 | 
			
		||||
    status = -errno;
 | 
			
		||||
    goto error;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  process->pty = master;
 | 
			
		||||
  process->pid = pid;
 | 
			
		||||
  process->paused = true;
 | 
			
		||||
  process->read_cb = read_cb;
 | 
			
		||||
  process->exit_cb = exit_cb;
 | 
			
		||||
  process->async.data = process;
 | 
			
		||||
  uv_async_init(process->loop, &process->async, async_cb);
 | 
			
		||||
  uv_thread_create(&process->tid, wait_cb, process);
 | 
			
		||||
 | 
			
		||||
  return 0;
 | 
			
		||||
 | 
			
		||||
error:
 | 
			
		||||
  close(master);
 | 
			
		||||
  uv_kill(pid, SIGKILL);
 | 
			
		||||
  waitpid(pid, NULL, 0);
 | 
			
		||||
  return status;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										68
									
								
								src/pty.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/pty.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
#ifndef TTYD_PTY_H
 | 
			
		||||
#define TTYD_PTY_H
 | 
			
		||||
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <stdint.h>
 | 
			
		||||
#include <uv.h>
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
#ifndef HPCON
 | 
			
		||||
#define HPCON VOID *
 | 
			
		||||
#endif
 | 
			
		||||
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
 | 
			
		||||
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool conpty_init();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
  char *base;
 | 
			
		||||
  size_t len;
 | 
			
		||||
} pty_buf_t;
 | 
			
		||||
 | 
			
		||||
struct pty_process_;
 | 
			
		||||
typedef struct pty_process_ pty_process;
 | 
			
		||||
typedef void (*pty_read_cb)(pty_process *, pty_buf_t *, bool);
 | 
			
		||||
typedef void (*pty_exit_cb)(pty_process *);
 | 
			
		||||
 | 
			
		||||
struct pty_process_ {
 | 
			
		||||
  int pid, exit_code, exit_signal;
 | 
			
		||||
  uint16_t columns, rows;
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
  STARTUPINFOEXW si;
 | 
			
		||||
  HPCON pty;
 | 
			
		||||
  HANDLE handle;
 | 
			
		||||
  HANDLE wait;
 | 
			
		||||
#else
 | 
			
		||||
  pid_t pty;
 | 
			
		||||
  uv_thread_t tid;
 | 
			
		||||
#endif
 | 
			
		||||
  char **argv;
 | 
			
		||||
  char **envp;
 | 
			
		||||
  char *cwd;
 | 
			
		||||
 | 
			
		||||
  uv_loop_t *loop;
 | 
			
		||||
  uv_async_t async;
 | 
			
		||||
  uv_pipe_t *in;
 | 
			
		||||
  uv_pipe_t *out;
 | 
			
		||||
  bool paused;
 | 
			
		||||
 | 
			
		||||
  pty_read_cb read_cb;
 | 
			
		||||
  pty_exit_cb exit_cb;
 | 
			
		||||
  void *ctx;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pty_buf_t *pty_buf_init(char *base, size_t len);
 | 
			
		||||
void pty_buf_free(pty_buf_t *buf);
 | 
			
		||||
pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]);
 | 
			
		||||
bool process_running(pty_process *process);
 | 
			
		||||
void process_free(pty_process *process);
 | 
			
		||||
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb);
 | 
			
		||||
void pty_pause(pty_process *process);
 | 
			
		||||
void pty_resume(pty_process *process);
 | 
			
		||||
int pty_write(pty_process *process, pty_buf_t *buf);
 | 
			
		||||
bool pty_resize(pty_process *process);
 | 
			
		||||
bool pty_kill(pty_process *process, int sig);
 | 
			
		||||
 | 
			
		||||
#endif  // TTYD_PTY_H
 | 
			
		||||
							
								
								
									
										634
									
								
								src/server.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										634
									
								
								src/server.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,634 @@
 | 
			
		||||
#include "server.h"
 | 
			
		||||
 | 
			
		||||
#include <errno.h>
 | 
			
		||||
#include <getopt.h>
 | 
			
		||||
#include <json.h>
 | 
			
		||||
#include <libwebsockets.h>
 | 
			
		||||
#include <signal.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
#include <sys/stat.h>
 | 
			
		||||
 | 
			
		||||
#include "utils.h"
 | 
			
		||||
 | 
			
		||||
#ifndef TTYD_VERSION
 | 
			
		||||
#define TTYD_VERSION "unknown"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
volatile bool force_exit = false;
 | 
			
		||||
struct lws_context *context;
 | 
			
		||||
struct server *server;
 | 
			
		||||
struct endpoints endpoints = {"/ws", "/", "/token", ""};
 | 
			
		||||
 | 
			
		||||
extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
 | 
			
		||||
extern int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
 | 
			
		||||
 | 
			
		||||
// websocket protocols
 | 
			
		||||
static const struct lws_protocols protocols[] = {{"http-only", callback_http, sizeof(struct pss_http), 0},
 | 
			
		||||
                                                 {"tty", callback_tty, sizeof(struct pss_tty), 0},
 | 
			
		||||
                                                 {NULL, NULL, 0, 0}};
 | 
			
		||||
 | 
			
		||||
#ifndef LWS_WITHOUT_EXTENSIONS
 | 
			
		||||
// websocket extensions
 | 
			
		||||
static const struct lws_extension extensions[] = {
 | 
			
		||||
    {"permessage-deflate", lws_extension_callback_pm_deflate, "permessage-deflate"},
 | 
			
		||||
    {"deflate-frame", lws_extension_callback_pm_deflate, "deflate_frame"},
 | 
			
		||||
    {NULL, NULL, NULL}};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
 | 
			
		||||
static const uint32_t backoff_ms[] = {1000, 2000, 3000, 4000, 5000};
 | 
			
		||||
static lws_retry_bo_t retry = {
 | 
			
		||||
    .retry_ms_table = backoff_ms,
 | 
			
		||||
    .retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms),
 | 
			
		||||
    .conceal_count = LWS_ARRAY_SIZE(backoff_ms),
 | 
			
		||||
    .secs_since_valid_ping = 5,
 | 
			
		||||
    .secs_since_valid_hangup = 10,
 | 
			
		||||
    .jitter_percent = 0,
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// command line options
 | 
			
		||||
static const struct option options[] = {{"port", required_argument, NULL, 'p'},
 | 
			
		||||
                                        {"interface", required_argument, NULL, 'i'},
 | 
			
		||||
                                        {"socket-owner", required_argument, NULL, 'U'},
 | 
			
		||||
                                        {"credential", required_argument, NULL, 'c'},
 | 
			
		||||
                                        {"auth-header", required_argument, NULL, 'H'},
 | 
			
		||||
                                        {"uid", required_argument, NULL, 'u'},
 | 
			
		||||
                                        {"gid", required_argument, NULL, 'g'},
 | 
			
		||||
                                        {"signal", required_argument, NULL, 's'},
 | 
			
		||||
                                        {"cwd", required_argument, NULL, 'w'},
 | 
			
		||||
                                        {"index", required_argument, NULL, 'I'},
 | 
			
		||||
                                        {"base-path", required_argument, NULL, 'b'},
 | 
			
		||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
 | 
			
		||||
                                        {"ping-interval", required_argument, NULL, 'P'},
 | 
			
		||||
#endif
 | 
			
		||||
                                        {"srv-buf-size", required_argument, NULL, 'f'},
 | 
			
		||||
                                        {"ipv6", no_argument, NULL, '6'},
 | 
			
		||||
                                        {"ssl", no_argument, NULL, 'S'},
 | 
			
		||||
                                        {"ssl-cert", required_argument, NULL, 'C'},
 | 
			
		||||
                                        {"ssl-key", required_argument, NULL, 'K'},
 | 
			
		||||
                                        {"ssl-ca", required_argument, NULL, 'A'},
 | 
			
		||||
                                        {"url-arg", no_argument, NULL, 'a'},
 | 
			
		||||
                                        {"writable", no_argument, NULL, 'W'},
 | 
			
		||||
                                        {"terminal-type", required_argument, NULL, 'T'},
 | 
			
		||||
                                        {"client-option", required_argument, NULL, 't'},
 | 
			
		||||
                                        {"check-origin", no_argument, NULL, 'O'},
 | 
			
		||||
                                        {"max-clients", required_argument, NULL, 'm'},
 | 
			
		||||
                                        {"once", no_argument, NULL, 'o'},
 | 
			
		||||
                                        {"exit-no-conn", no_argument, NULL, 'q'},
 | 
			
		||||
                                        {"browser", no_argument, NULL, 'B'},
 | 
			
		||||
                                        {"debug", required_argument, NULL, 'd'},
 | 
			
		||||
                                        {"version", no_argument, NULL, 'v'},
 | 
			
		||||
                                        {"help", no_argument, NULL, 'h'},
 | 
			
		||||
                                        {NULL, 0, 0, 0}};
 | 
			
		||||
static const char *opt_string = "p:i:U:c:H:u:g:s:w:I:b:P:f:6aSC:K:A:Wt:T:Om:oqBd:vh";
 | 
			
		||||
 | 
			
		||||
static void print_help() {
 | 
			
		||||
  // clang-format off
 | 
			
		||||
  fprintf(stderr, "ttyd is a tool for sharing terminal over the web\n\n"
 | 
			
		||||
          "USAGE:\n"
 | 
			
		||||
          "    ttyd [options] <command> [<arguments...>]\n\n"
 | 
			
		||||
          "VERSION:\n"
 | 
			
		||||
          "    %s\n\n"
 | 
			
		||||
          "OPTIONS:\n"
 | 
			
		||||
          "    -p, --port              Port to listen (default: 7681, use `0` for random port)\n"
 | 
			
		||||
          "    -i, --interface         Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)\n"
 | 
			
		||||
          "    -U, --socket-owner      User owner of the UNIX domain socket file, when enabled (eg: user:group)\n"
 | 
			
		||||
          "    -c, --credential        Credential for basic authentication (format: username:password)\n"
 | 
			
		||||
          "    -H, --auth-header       HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication\n"
 | 
			
		||||
          "    -u, --uid               User id to run with\n"
 | 
			
		||||
          "    -g, --gid               Group id to run with\n"
 | 
			
		||||
          "    -s, --signal            Signal to send to the command when exit it (default: 1, SIGHUP)\n"
 | 
			
		||||
          "    -w, --cwd               Working directory to be set for the child program\n"
 | 
			
		||||
          "    -a, --url-arg           Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)\n"
 | 
			
		||||
          "    -W, --writable          Allow clients to write to the TTY (readonly by default)\n"
 | 
			
		||||
          "    -t, --client-option     Send option to client (format: key=value), repeat to add more options\n"
 | 
			
		||||
          "    -T, --terminal-type     Terminal type to report, default: xterm-256color\n"
 | 
			
		||||
          "    -O, --check-origin      Do not allow websocket connection from different origin\n"
 | 
			
		||||
          "    -m, --max-clients       Maximum clients to support (default: 0, no limit)\n"
 | 
			
		||||
          "    -o, --once              Accept only one client and exit on disconnection\n"
 | 
			
		||||
          "    -q, --exit-no-conn      Exit on all clients disconnection\n"
 | 
			
		||||
          "    -B, --browser           Open terminal with the default system browser\n"
 | 
			
		||||
          "    -I, --index             Custom index.html path\n"
 | 
			
		||||
          "    -b, --base-path         Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)\n"
 | 
			
		||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
 | 
			
		||||
          "    -P, --ping-interval     Websocket ping interval(sec) (default: 5)\n"
 | 
			
		||||
#endif
 | 
			
		||||
          "    -f, --srv-buf-size      Maximum chunk of file (in bytes) that can be sent at once, a larger value may improve throughput (default: 4096)\n"
 | 
			
		||||
#ifdef LWS_WITH_IPV6
 | 
			
		||||
          "    -6, --ipv6              Enable IPv6 support\n"
 | 
			
		||||
#endif
 | 
			
		||||
#if defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)
 | 
			
		||||
          "    -S, --ssl               Enable SSL\n"
 | 
			
		||||
          "    -C, --ssl-cert          SSL certificate file path\n"
 | 
			
		||||
          "    -K, --ssl-key           SSL key file path\n"
 | 
			
		||||
          "    -A, --ssl-ca            SSL CA file path for client certificate verification\n"
 | 
			
		||||
#endif
 | 
			
		||||
          "    -d, --debug             Set log level (default: 7)\n"
 | 
			
		||||
          "    -v, --version           Print the version and exit\n"
 | 
			
		||||
          "    -h, --help              Print this text and exit\n\n"
 | 
			
		||||
          "Visit https://github.com/tsl0922/ttyd to get more information and report bugs.\n",
 | 
			
		||||
          TTYD_VERSION
 | 
			
		||||
  );
 | 
			
		||||
  // clang-format on
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void print_config() {
 | 
			
		||||
  lwsl_notice("tty configuration:\n");
 | 
			
		||||
  if (server->credential != NULL) lwsl_notice("  credential: %s\n", server->credential);
 | 
			
		||||
  lwsl_notice("  start command: %s\n", server->command);
 | 
			
		||||
  lwsl_notice("  close signal: %s (%d)\n", server->sig_name, server->sig_code);
 | 
			
		||||
  lwsl_notice("  terminal type: %s\n", server->terminal_type);
 | 
			
		||||
  if (endpoints.parent[0]) {
 | 
			
		||||
    lwsl_notice("endpoints:\n");
 | 
			
		||||
    lwsl_notice("  base-path: %s\n", endpoints.parent);
 | 
			
		||||
    lwsl_notice("  index    : %s\n", endpoints.index);
 | 
			
		||||
    lwsl_notice("  token    : %s\n", endpoints.token);
 | 
			
		||||
    lwsl_notice("  websocket: %s\n", endpoints.ws);
 | 
			
		||||
  }
 | 
			
		||||
  if (server->auth_header != NULL) lwsl_notice("  auth header: %s\n", server->auth_header);
 | 
			
		||||
  if (server->check_origin) lwsl_notice("  check origin: true\n");
 | 
			
		||||
  if (server->url_arg) lwsl_notice("  allow url arg: true\n");
 | 
			
		||||
  if (server->max_clients > 0) lwsl_notice("  max clients: %d\n", server->max_clients);
 | 
			
		||||
  if (server->once) lwsl_notice("  once: true\n");
 | 
			
		||||
  if (server->exit_no_conn) lwsl_notice("  exit_no_conn: true\n");
 | 
			
		||||
  if (server->index != NULL) lwsl_notice("  custom index.html: %s\n", server->index);
 | 
			
		||||
  if (server->cwd != NULL) lwsl_notice("  working directory: %s\n", server->cwd);
 | 
			
		||||
  if (!server->writable) lwsl_warn("The --writable option is not set, will start in readonly mode\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static struct server *server_new(int argc, char **argv, int start) {
 | 
			
		||||
  struct server *ts;
 | 
			
		||||
  size_t cmd_len = 0;
 | 
			
		||||
 | 
			
		||||
  ts = xmalloc(sizeof(struct server));
 | 
			
		||||
 | 
			
		||||
  memset(ts, 0, sizeof(struct server));
 | 
			
		||||
  ts->client_count = 0;
 | 
			
		||||
  ts->sig_code = SIGHUP;
 | 
			
		||||
  sprintf(ts->terminal_type, "%s", "xterm-256color");
 | 
			
		||||
  get_sig_name(ts->sig_code, ts->sig_name, sizeof(ts->sig_name));
 | 
			
		||||
  if (start == argc) return ts;
 | 
			
		||||
 | 
			
		||||
  int cmd_argc = argc - start;
 | 
			
		||||
  char **cmd_argv = &argv[start];
 | 
			
		||||
  ts->argv = xmalloc(sizeof(char *) * (cmd_argc + 1));
 | 
			
		||||
  for (int i = 0; i < cmd_argc; i++) {
 | 
			
		||||
    ts->argv[i] = strdup(cmd_argv[i]);
 | 
			
		||||
    cmd_len += strlen(ts->argv[i]);
 | 
			
		||||
    if (i != cmd_argc - 1) {
 | 
			
		||||
      cmd_len++;  // for space
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ts->argv[cmd_argc] = NULL;
 | 
			
		||||
  ts->argc = cmd_argc;
 | 
			
		||||
 | 
			
		||||
  ts->command = xmalloc(cmd_len + 1);
 | 
			
		||||
  char *ptr = ts->command;
 | 
			
		||||
  for (int i = 0; i < cmd_argc; i++) {
 | 
			
		||||
    size_t len = strlen(ts->argv[i]);
 | 
			
		||||
    ptr = memcpy(ptr, ts->argv[i], len + 1) + len;
 | 
			
		||||
    if (i != cmd_argc - 1) {
 | 
			
		||||
      *ptr++ = ' ';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  *ptr = '\0';  // null terminator
 | 
			
		||||
 | 
			
		||||
  ts->loop = xmalloc(sizeof *ts->loop);
 | 
			
		||||
  uv_loop_init(ts->loop);
 | 
			
		||||
 | 
			
		||||
  return ts;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void server_free(struct server *ts) {
 | 
			
		||||
  if (ts == NULL) return;
 | 
			
		||||
  if (ts->credential != NULL) free(ts->credential);
 | 
			
		||||
  if (ts->auth_header != NULL) free(ts->auth_header);
 | 
			
		||||
  if (ts->index != NULL) free(ts->index);
 | 
			
		||||
  if (ts->cwd != NULL) free(ts->cwd);
 | 
			
		||||
  free(ts->command);
 | 
			
		||||
  free(ts->prefs_json);
 | 
			
		||||
 | 
			
		||||
  char **p = ts->argv;
 | 
			
		||||
  for (; *p; p++) free(*p);
 | 
			
		||||
  free(ts->argv);
 | 
			
		||||
 | 
			
		||||
  if (strlen(ts->socket_path) > 0) {
 | 
			
		||||
    struct stat st;
 | 
			
		||||
    if (!stat(ts->socket_path, &st)) {
 | 
			
		||||
      unlink(ts->socket_path);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uv_loop_close(ts->loop);
 | 
			
		||||
 | 
			
		||||
  free(ts->loop);
 | 
			
		||||
  free(ts);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void signal_cb(uv_signal_t *watcher, int signum) {
 | 
			
		||||
  char sig_name[20];
 | 
			
		||||
 | 
			
		||||
  switch (watcher->signum) {
 | 
			
		||||
    case SIGINT:
 | 
			
		||||
    case SIGTERM:
 | 
			
		||||
      get_sig_name(watcher->signum, sig_name, sizeof(sig_name));
 | 
			
		||||
      lwsl_notice("received signal: %s (%d), exiting...\n", sig_name, watcher->signum);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      signal(SIGABRT, SIG_DFL);
 | 
			
		||||
      abort();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (force_exit) exit(EXIT_FAILURE);
 | 
			
		||||
  force_exit = true;
 | 
			
		||||
 | 
			
		||||
  lws_cancel_service(context);
 | 
			
		||||
  uv_stop(server->loop);
 | 
			
		||||
 | 
			
		||||
  lwsl_notice("send ^C to force exit.\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static int parse_int(char *name, char *str) {
 | 
			
		||||
  char *endptr;
 | 
			
		||||
  errno = 0;
 | 
			
		||||
  long val = strtol(str, &endptr, 0);
 | 
			
		||||
  if (errno != 0 || endptr == str) {
 | 
			
		||||
    fprintf(stderr, "ttyd: invalid value for %s: %s\n", name, str);
 | 
			
		||||
    exit(EXIT_FAILURE);
 | 
			
		||||
  }
 | 
			
		||||
  return (int)val;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static int calc_command_start(int argc, char **argv) {
 | 
			
		||||
  // make a copy of argc and argv
 | 
			
		||||
  int argc_copy = argc;
 | 
			
		||||
  char **argv_copy = xmalloc(sizeof(char *) * argc);
 | 
			
		||||
  for (int i = 0; i < argc; i++) {
 | 
			
		||||
    argv_copy[i] = strdup(argv[i]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // do not print error message for invalid option
 | 
			
		||||
  opterr = 0;
 | 
			
		||||
  while (getopt_long(argc_copy, argv_copy, opt_string, options, NULL) != -1)
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
  int start = argc;
 | 
			
		||||
  if (optind < argc) {
 | 
			
		||||
    char *command = argv_copy[optind];
 | 
			
		||||
    for (int i = 0; i < argc; i++) {
 | 
			
		||||
      if (strcmp(argv[i], command) == 0) {
 | 
			
		||||
        start = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // free argv copy
 | 
			
		||||
  for (int i = 0; i < argc; i++) {
 | 
			
		||||
    free(argv_copy[i]);
 | 
			
		||||
  }
 | 
			
		||||
  free(argv_copy);
 | 
			
		||||
 | 
			
		||||
  // reset for next use
 | 
			
		||||
  opterr = 1;
 | 
			
		||||
  optind = 0;
 | 
			
		||||
 | 
			
		||||
  return start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int main(int argc, char **argv) {
 | 
			
		||||
  if (argc == 1) {
 | 
			
		||||
    print_help();
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
  if (!conpty_init()) {
 | 
			
		||||
    fprintf(stderr, "ERROR: ConPTY init failed! Make sure you are on Windows 10 1809 or later.");
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  int start = calc_command_start(argc, argv);
 | 
			
		||||
  server = server_new(argc, argv, start);
 | 
			
		||||
 | 
			
		||||
  struct lws_context_creation_info info;
 | 
			
		||||
  memset(&info, 0, sizeof(info));
 | 
			
		||||
  info.port = 7681;
 | 
			
		||||
  info.iface = NULL;
 | 
			
		||||
  info.protocols = protocols;
 | 
			
		||||
  info.gid = -1;
 | 
			
		||||
  info.uid = -1;
 | 
			
		||||
  info.max_http_header_pool = 16;
 | 
			
		||||
  info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6;
 | 
			
		||||
#ifndef LWS_WITHOUT_EXTENSIONS
 | 
			
		||||
  info.extensions = extensions;
 | 
			
		||||
#endif
 | 
			
		||||
  info.max_http_header_data = 65535;
 | 
			
		||||
 | 
			
		||||
  int debug_level = LLL_ERR | LLL_WARN | LLL_NOTICE;
 | 
			
		||||
  char iface[128] = "";
 | 
			
		||||
  char socket_owner[128] = "";
 | 
			
		||||
  bool browser = false;
 | 
			
		||||
  bool ssl = false;
 | 
			
		||||
  char cert_path[1024] = "";
 | 
			
		||||
  char key_path[1024] = "";
 | 
			
		||||
  char ca_path[1024] = "";
 | 
			
		||||
 | 
			
		||||
  struct json_object *client_prefs = json_object_new_object();
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
  json_object_object_add(client_prefs, "isWindows", json_object_new_boolean(true));
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // parse command line options
 | 
			
		||||
  int c;
 | 
			
		||||
  while ((c = getopt_long(start, argv, opt_string, options, NULL)) != -1) {
 | 
			
		||||
    switch (c) {
 | 
			
		||||
      case 'h':
 | 
			
		||||
        print_help();
 | 
			
		||||
        return 0;
 | 
			
		||||
      case 'v':
 | 
			
		||||
        printf("ttyd version %s\n", TTYD_VERSION);
 | 
			
		||||
        return 0;
 | 
			
		||||
      case 'd':
 | 
			
		||||
        debug_level = parse_int("debug", optarg);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'a':
 | 
			
		||||
        server->url_arg = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'W':
 | 
			
		||||
        server->writable = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'O':
 | 
			
		||||
        server->check_origin = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'm':
 | 
			
		||||
        server->max_clients = parse_int("max-clients", optarg);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'o':
 | 
			
		||||
        server->once = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'q':
 | 
			
		||||
        server->exit_no_conn = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'B':
 | 
			
		||||
        browser = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'p':
 | 
			
		||||
        info.port = parse_int("port", optarg);
 | 
			
		||||
        if (info.port < 0) {
 | 
			
		||||
          fprintf(stderr, "ttyd: invalid port: %s\n", optarg);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 'i':
 | 
			
		||||
        strncpy(iface, optarg, sizeof(iface) - 1);
 | 
			
		||||
        iface[sizeof(iface) - 1] = '\0';
 | 
			
		||||
        break;
 | 
			
		||||
      case 'U':
 | 
			
		||||
        strncpy(socket_owner, optarg, sizeof(socket_owner) - 1);
 | 
			
		||||
        socket_owner[sizeof(socket_owner) - 1] = '\0';
 | 
			
		||||
        break;
 | 
			
		||||
      case 'c':
 | 
			
		||||
        if (strchr(optarg, ':') == NULL) {
 | 
			
		||||
          fprintf(stderr, "ttyd: invalid credential, format: username:password\n");
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        char b64_text[256];
 | 
			
		||||
        lws_b64_encode_string(optarg, strlen(optarg), b64_text, sizeof(b64_text));
 | 
			
		||||
        server->credential = strdup(b64_text);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'H':
 | 
			
		||||
        server->auth_header = strdup(optarg);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'u':
 | 
			
		||||
        info.uid = parse_int("uid", optarg);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'g':
 | 
			
		||||
        info.gid = parse_int("gid", optarg);
 | 
			
		||||
        break;
 | 
			
		||||
      case 's': {
 | 
			
		||||
        int sig = get_sig(optarg);
 | 
			
		||||
        if (sig > 0) {
 | 
			
		||||
          server->sig_code = sig;
 | 
			
		||||
          get_sig_name(sig, server->sig_name, sizeof(server->sig_name));
 | 
			
		||||
        } else {
 | 
			
		||||
          fprintf(stderr, "ttyd: invalid signal: %s\n", optarg);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
      } break;
 | 
			
		||||
      case 'w':
 | 
			
		||||
        server->cwd = strdup(optarg);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'I':
 | 
			
		||||
        if (!strncmp(optarg, "~/", 2)) {
 | 
			
		||||
          const char *home = getenv("HOME");
 | 
			
		||||
          server->index = malloc(strlen(home) + strlen(optarg) - 1);
 | 
			
		||||
          sprintf(server->index, "%s%s", home, optarg + 1);
 | 
			
		||||
        } else {
 | 
			
		||||
          server->index = strdup(optarg);
 | 
			
		||||
        }
 | 
			
		||||
        struct stat st;
 | 
			
		||||
        if (stat(server->index, &st) == -1) {
 | 
			
		||||
          fprintf(stderr, "Can not stat index.html: %s, error: %s\n", server->index, strerror(errno));
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        if (S_ISDIR(st.st_mode)) {
 | 
			
		||||
          fprintf(stderr, "Invalid index.html path: %s, is it a dir?\n", server->index);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 'b': {
 | 
			
		||||
        char path[128];
 | 
			
		||||
        strncpy(path, optarg, 128);
 | 
			
		||||
        size_t len = strlen(path);
 | 
			
		||||
        while (len && path[len - 1] == '/') path[--len] = 0;  // trim trailing /
 | 
			
		||||
        if (!len) break;
 | 
			
		||||
#define sc(f)                                  \
 | 
			
		||||
  strncpy(path + len, endpoints.f, 128 - len); \
 | 
			
		||||
  endpoints.f = strdup(path);
 | 
			
		||||
        sc(ws) sc(index) sc(token) sc(parent)
 | 
			
		||||
#undef sc
 | 
			
		||||
      } break;
 | 
			
		||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
 | 
			
		||||
      case 'P': {
 | 
			
		||||
        int interval = parse_int("ping-interval", optarg);
 | 
			
		||||
        if (interval < 0) {
 | 
			
		||||
          fprintf(stderr, "ttyd: invalid ping interval: %s\n", optarg);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        retry.secs_since_valid_ping = interval;
 | 
			
		||||
        retry.secs_since_valid_hangup = interval + 7;
 | 
			
		||||
      } break;
 | 
			
		||||
#endif
 | 
			
		||||
      case 'f': {
 | 
			
		||||
        int serv_buf_size = parse_int("srv-buf-size", optarg);
 | 
			
		||||
        if (serv_buf_size < 0) {
 | 
			
		||||
          fprintf(stderr, "ttyd: invalid srv-buf-size: %s\n", optarg);
 | 
			
		||||
          return -1;
 | 
			
		||||
        }
 | 
			
		||||
        info.pt_serv_buf_size = serv_buf_size;
 | 
			
		||||
      } break;
 | 
			
		||||
      case '6':
 | 
			
		||||
        info.options &= ~(LWS_SERVER_OPTION_DISABLE_IPV6);
 | 
			
		||||
        break;
 | 
			
		||||
#if defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)
 | 
			
		||||
      case 'S':
 | 
			
		||||
        ssl = true;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'C':
 | 
			
		||||
        strncpy(cert_path, optarg, sizeof(cert_path) - 1);
 | 
			
		||||
        cert_path[sizeof(cert_path) - 1] = '\0';
 | 
			
		||||
        break;
 | 
			
		||||
      case 'K':
 | 
			
		||||
        strncpy(key_path, optarg, sizeof(key_path) - 1);
 | 
			
		||||
        key_path[sizeof(key_path) - 1] = '\0';
 | 
			
		||||
        break;
 | 
			
		||||
      case 'A':
 | 
			
		||||
        strncpy(ca_path, optarg, sizeof(ca_path) - 1);
 | 
			
		||||
        ca_path[sizeof(ca_path) - 1] = '\0';
 | 
			
		||||
        break;
 | 
			
		||||
#endif
 | 
			
		||||
      case 'T':
 | 
			
		||||
        strncpy(server->terminal_type, optarg, sizeof(server->terminal_type) - 1);
 | 
			
		||||
        server->terminal_type[sizeof(server->terminal_type) - 1] = '\0';
 | 
			
		||||
        break;
 | 
			
		||||
      case '?':
 | 
			
		||||
        break;
 | 
			
		||||
      case 't':
 | 
			
		||||
        optind--;
 | 
			
		||||
        for (; optind < start && *argv[optind] != '-'; optind++) {
 | 
			
		||||
          char *option = optarg;
 | 
			
		||||
          char *key = strsep(&option, "=");
 | 
			
		||||
          if (key == NULL) {
 | 
			
		||||
            fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
 | 
			
		||||
            return -1;
 | 
			
		||||
          }
 | 
			
		||||
          char *value = strsep(&option, "=");
 | 
			
		||||
          if (value == NULL) {
 | 
			
		||||
            fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
 | 
			
		||||
            return -1;
 | 
			
		||||
          }
 | 
			
		||||
          struct json_object *obj = json_tokener_parse(value);
 | 
			
		||||
          json_object_object_add(client_prefs, key, obj != NULL ? obj : json_object_new_string(value));
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        print_help();
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  server->prefs_json = strdup(json_object_to_json_string(client_prefs));
 | 
			
		||||
  json_object_put(client_prefs);
 | 
			
		||||
 | 
			
		||||
  if (server->command == NULL || strlen(server->command) == 0) {
 | 
			
		||||
    fprintf(stderr, "ttyd: missing start command\n");
 | 
			
		||||
    return -1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  lws_set_log_level(debug_level, NULL);
 | 
			
		||||
 | 
			
		||||
  char server_hdr[128] = "";
 | 
			
		||||
  sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION, LWS_LIBRARY_VERSION);
 | 
			
		||||
  info.server_string = server_hdr;
 | 
			
		||||
 | 
			
		||||
#if LWS_LIBRARY_VERSION_NUMBER < 4000000
 | 
			
		||||
  info.ws_ping_pong_interval = 5;
 | 
			
		||||
#else
 | 
			
		||||
  info.retry_and_idle_policy = &retry;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (strlen(iface) > 0) {
 | 
			
		||||
    info.iface = iface;
 | 
			
		||||
    if (endswith(info.iface, ".sock") || endswith(info.iface, ".socket")) {
 | 
			
		||||
#if defined(LWS_USE_UNIX_SOCK) || defined(LWS_WITH_UNIX_SOCK)
 | 
			
		||||
      info.options |= LWS_SERVER_OPTION_UNIX_SOCK;
 | 
			
		||||
      info.port = 0;  // warmcat/libwebsockets#1985
 | 
			
		||||
      strncpy(server->socket_path, info.iface, sizeof(server->socket_path) - 1);
 | 
			
		||||
      if (strlen(socket_owner) > 0) {
 | 
			
		||||
        info.unix_socket_perms = socket_owner;
 | 
			
		||||
      }
 | 
			
		||||
#else
 | 
			
		||||
      fprintf(stderr, "libwebsockets is not compiled with UNIX domain socket support");
 | 
			
		||||
      return -1;
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#if defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)
 | 
			
		||||
  if (ssl) {
 | 
			
		||||
    info.ssl_cert_filepath = cert_path;
 | 
			
		||||
    info.ssl_private_key_filepath = key_path;
 | 
			
		||||
#ifndef LWS_WITH_MBEDTLS
 | 
			
		||||
    info.ssl_options_set = SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
 | 
			
		||||
#endif
 | 
			
		||||
    if (strlen(ca_path) > 0) {
 | 
			
		||||
      info.ssl_ca_filepath = ca_path;
 | 
			
		||||
      info.options |= LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT;
 | 
			
		||||
    }
 | 
			
		||||
    info.options |= LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT | LWS_SERVER_OPTION_REDIRECT_HTTP_TO_HTTPS;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  lwsl_notice("ttyd %s (libwebsockets %s)\n", TTYD_VERSION, LWS_LIBRARY_VERSION);
 | 
			
		||||
  print_config();
 | 
			
		||||
 | 
			
		||||
  // lws custom header requires lower case name, and terminating :
 | 
			
		||||
  if (server->auth_header != NULL) {
 | 
			
		||||
    size_t auth_header_len = strlen(server->auth_header);
 | 
			
		||||
    server->auth_header = xrealloc(server->auth_header, auth_header_len + 2);
 | 
			
		||||
    strcat(server->auth_header + auth_header_len, ":");
 | 
			
		||||
    lowercase(server->auth_header);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void *foreign_loops[1];
 | 
			
		||||
  foreign_loops[0] = server->loop;
 | 
			
		||||
  info.foreign_loops = foreign_loops;
 | 
			
		||||
  info.options |= LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
 | 
			
		||||
 | 
			
		||||
  context = lws_create_context(&info);
 | 
			
		||||
  if (context == NULL) {
 | 
			
		||||
    lwsl_err("libwebsockets context creation failed\n");
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  struct lws_vhost *vhost = lws_create_vhost(context, &info);
 | 
			
		||||
  if (vhost == NULL) {
 | 
			
		||||
    lwsl_err("libwebsockets vhost creation failed\n");
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
  int port = lws_get_vhost_listen_port(vhost);
 | 
			
		||||
  lwsl_notice(" Listening on port: %d\n", port);
 | 
			
		||||
 | 
			
		||||
  if (browser) {
 | 
			
		||||
    char url[30];
 | 
			
		||||
    sprintf(url, "%s://localhost:%d", ssl ? "https" : "http", port);
 | 
			
		||||
    open_uri(url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define sig_count 2
 | 
			
		||||
  int sig_nums[] = {SIGINT, SIGTERM};
 | 
			
		||||
  uv_signal_t signals[sig_count];
 | 
			
		||||
  for (int i = 0; i < sig_count; i++) {
 | 
			
		||||
    uv_signal_init(server->loop, &signals[i]);
 | 
			
		||||
    uv_signal_start(&signals[i], signal_cb, sig_nums[i]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  lws_service(context, 0);
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; i < sig_count; i++) {
 | 
			
		||||
    uv_signal_stop(&signals[i]);
 | 
			
		||||
  }
 | 
			
		||||
#undef sig_count
 | 
			
		||||
 | 
			
		||||
  lws_context_destroy(context);
 | 
			
		||||
 | 
			
		||||
  // cleanup
 | 
			
		||||
  server_free(server);
 | 
			
		||||
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										86
									
								
								src/server.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/server.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
#include <libwebsockets.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <uv.h>
 | 
			
		||||
 | 
			
		||||
#include "pty.h"
 | 
			
		||||
 | 
			
		||||
// client message
 | 
			
		||||
#define INPUT '0'
 | 
			
		||||
#define RESIZE_TERMINAL '1'
 | 
			
		||||
#define PAUSE '2'
 | 
			
		||||
#define RESUME '3'
 | 
			
		||||
#define JSON_DATA '{'
 | 
			
		||||
 | 
			
		||||
// server message
 | 
			
		||||
#define OUTPUT '0'
 | 
			
		||||
#define SET_WINDOW_TITLE '1'
 | 
			
		||||
#define SET_PREFERENCES '2'
 | 
			
		||||
 | 
			
		||||
// url paths
 | 
			
		||||
struct endpoints {
 | 
			
		||||
  char *ws;
 | 
			
		||||
  char *index;
 | 
			
		||||
  char *token;
 | 
			
		||||
  char *parent;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern volatile bool force_exit;
 | 
			
		||||
extern struct lws_context *context;
 | 
			
		||||
extern struct server *server;
 | 
			
		||||
extern struct endpoints endpoints;
 | 
			
		||||
 | 
			
		||||
struct pss_http {
 | 
			
		||||
  char path[128];
 | 
			
		||||
  char *buffer;
 | 
			
		||||
  char *ptr;
 | 
			
		||||
  size_t len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct pss_tty {
 | 
			
		||||
  bool initialized;
 | 
			
		||||
  int initial_cmd_index;
 | 
			
		||||
  bool authenticated;
 | 
			
		||||
  char user[30];
 | 
			
		||||
  char address[50];
 | 
			
		||||
  char path[128];
 | 
			
		||||
  char **args;
 | 
			
		||||
  int argc;
 | 
			
		||||
 | 
			
		||||
  struct lws *wsi;
 | 
			
		||||
  char *buffer;
 | 
			
		||||
  size_t len;
 | 
			
		||||
 | 
			
		||||
  pty_process *process;
 | 
			
		||||
  pty_buf_t *pty_buf;
 | 
			
		||||
 | 
			
		||||
  int lws_close_status;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
  struct pss_tty *pss;
 | 
			
		||||
  bool ws_closed;
 | 
			
		||||
} pty_ctx_t;
 | 
			
		||||
 | 
			
		||||
struct server {
 | 
			
		||||
  int client_count;        // client count
 | 
			
		||||
  char *prefs_json;        // client preferences
 | 
			
		||||
  char *credential;        // encoded basic auth credential
 | 
			
		||||
  char *auth_header;       // header name used for auth proxy
 | 
			
		||||
  char *index;             // custom index.html
 | 
			
		||||
  char *command;           // full command line
 | 
			
		||||
  char **argv;             // command with arguments
 | 
			
		||||
  int argc;                // command + arguments count
 | 
			
		||||
  char *cwd;               // working directory
 | 
			
		||||
  int sig_code;            // close signal
 | 
			
		||||
  char sig_name[20];       // human readable signal string
 | 
			
		||||
  bool url_arg;            // allow client to send cli arguments in URL
 | 
			
		||||
  bool writable;           // whether clients to write to the TTY
 | 
			
		||||
  bool check_origin;       // whether allow websocket connection from different origin
 | 
			
		||||
  int max_clients;         // maximum clients to support
 | 
			
		||||
  bool once;               // whether accept only one client and exit on disconnection
 | 
			
		||||
  bool exit_no_conn;       // whether exit on all clients disconnection
 | 
			
		||||
  char socket_path[255];   // UNIX domain socket path
 | 
			
		||||
  char terminal_type[30];  // terminal type to report
 | 
			
		||||
 | 
			
		||||
  uv_loop_t *loop;         // the libuv event loop
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										163
									
								
								src/utils.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/utils.c
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
#include <ctype.h>
 | 
			
		||||
#include <fcntl.h>
 | 
			
		||||
#include <signal.h>
 | 
			
		||||
#include <stdbool.h>
 | 
			
		||||
#include <stdio.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
 | 
			
		||||
#if defined(__linux__) && !defined(__ANDROID__)
 | 
			
		||||
const char *sys_signame[NSIG] = {
 | 
			
		||||
    "zero", "HUP",  "INT",  "QUIT", "ILL",    "TRAP",   "ABRT",  "UNUSED", "FPE",  "KILL", "USR1",
 | 
			
		||||
    "SEGV", "USR2", "PIPE", "ALRM", "TERM",   "STKFLT", "CHLD",  "CONT",   "STOP", "TSTP", "TTIN",
 | 
			
		||||
    "TTOU", "URG",  "XCPU", "XFSZ", "VTALRM", "PROF",   "WINCH", "IO",     "PWR",  "SYS",  NULL};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(_WIN32) || defined(__CYGWIN__)
 | 
			
		||||
#include <windows.h>
 | 
			
		||||
#undef NSIG
 | 
			
		||||
#define NSIG 33
 | 
			
		||||
const char *sys_signame[NSIG] = {
 | 
			
		||||
    "zero", "HUP", "INT",  "QUIT", "ILL",    "TRAP", "IOT",   "EMT",  "FPE",  "KILL", "BUS",
 | 
			
		||||
    "SEGV", "SYS", "PIPE", "ALRM", "TERM",   "URG",  "STOP",  "TSTP", "CONT", "CHLD", "TTIN",
 | 
			
		||||
    "TTOU", "IO",  "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR",  "USR1", "USR2", NULL};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void *xmalloc(size_t size) {
 | 
			
		||||
  if (size == 0) return NULL;
 | 
			
		||||
  void *p = malloc(size);
 | 
			
		||||
  if (!p) abort();
 | 
			
		||||
  return p;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void *xrealloc(void *p, size_t size) {
 | 
			
		||||
  if ((size == 0) && (p == NULL)) return NULL;
 | 
			
		||||
  p = realloc(p, size);
 | 
			
		||||
  if (!p) abort();
 | 
			
		||||
  return p;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
char *uppercase(char *s) {
 | 
			
		||||
  while(*s) {
 | 
			
		||||
    *s = (char)toupper((int)*s);
 | 
			
		||||
    s++;
 | 
			
		||||
  }
 | 
			
		||||
  return s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
char *lowercase(char *s) {
 | 
			
		||||
  while(*s) {
 | 
			
		||||
    *s = (char)tolower((int)*s);
 | 
			
		||||
    s++;
 | 
			
		||||
  }
 | 
			
		||||
  return s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool endswith(const char *str, const char *suffix) {
 | 
			
		||||
  size_t str_len = strlen(str);
 | 
			
		||||
  size_t suffix_len = strlen(suffix);
 | 
			
		||||
  return str_len > suffix_len && !strcmp(str + (str_len - suffix_len), suffix);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int get_sig_name(int sig, char *buf, size_t len) {
 | 
			
		||||
  int n = snprintf(buf, len, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown");
 | 
			
		||||
  uppercase(buf);
 | 
			
		||||
  return n;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int get_sig(const char *sig_name) {
 | 
			
		||||
  for (int sig = 1; sig < NSIG; sig++) {
 | 
			
		||||
    const char *name = sys_signame[sig];
 | 
			
		||||
    if (name != NULL && (strcasecmp(name, sig_name) == 0 || strcasecmp(name, sig_name + 3) == 0))
 | 
			
		||||
      return sig;
 | 
			
		||||
  }
 | 
			
		||||
  return atoi(sig_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int open_uri(char *uri) {
 | 
			
		||||
#ifdef __APPLE__
 | 
			
		||||
  char command[256];
 | 
			
		||||
  sprintf(command, "open %s > /dev/null 2>&1", uri);
 | 
			
		||||
  return system(command);
 | 
			
		||||
#elif defined(_WIN32) || defined(__CYGWIN__)
 | 
			
		||||
  return ShellExecute(0, 0, uri, 0, 0, SW_SHOW) > (HINSTANCE)32 ? 0 : 1;
 | 
			
		||||
#else
 | 
			
		||||
  // check if X server is running
 | 
			
		||||
  if (system("xset -q > /dev/null 2>&1")) return 1;
 | 
			
		||||
  char command[256];
 | 
			
		||||
  sprintf(command, "xdg-open %s > /dev/null 2>&1", uri);
 | 
			
		||||
  return system(command);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
char *strsep(char **sp, char *sep) {
 | 
			
		||||
  char *p, *s;
 | 
			
		||||
  if (sp == NULL || *sp == NULL || **sp == '\0') return (NULL);
 | 
			
		||||
  s = *sp;
 | 
			
		||||
  p = s + strcspn(s, sep);
 | 
			
		||||
  if (*p != '\0') *p++ = '\0';
 | 
			
		||||
  *sp = p;
 | 
			
		||||
  return s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *quote_arg(const char *arg) {
 | 
			
		||||
  int len = 0, n = 0;
 | 
			
		||||
  int force_quotes = 0;
 | 
			
		||||
  char *q, *d;
 | 
			
		||||
  const char *p = arg;
 | 
			
		||||
  if (!*p) force_quotes = 1;
 | 
			
		||||
  while (*p) {
 | 
			
		||||
    if (isspace(*p) || *p == '*' || *p == '?' || *p == '{' || *p == '\'')
 | 
			
		||||
      force_quotes = 1;
 | 
			
		||||
    else if (*p == '"')
 | 
			
		||||
      n++;
 | 
			
		||||
    else if (*p == '\\') {
 | 
			
		||||
      int count = 0;
 | 
			
		||||
      while (*p == '\\') {
 | 
			
		||||
        count++;
 | 
			
		||||
        p++;
 | 
			
		||||
        len++;
 | 
			
		||||
      }
 | 
			
		||||
      if (*p == '"' || !*p) n += count * 2 + 1;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    len++;
 | 
			
		||||
    p++;
 | 
			
		||||
  }
 | 
			
		||||
  if (!force_quotes && n == 0) return arg;
 | 
			
		||||
 | 
			
		||||
  d = q = xmalloc(len + n + 3);
 | 
			
		||||
  *d++ = '"';
 | 
			
		||||
  while (*arg) {
 | 
			
		||||
    if (*arg == '"')
 | 
			
		||||
      *d++ = '\\';
 | 
			
		||||
    else if (*arg == '\\') {
 | 
			
		||||
      int count = 0;
 | 
			
		||||
      while (*arg == '\\') {
 | 
			
		||||
        count++;
 | 
			
		||||
        *d++ = *arg++;
 | 
			
		||||
      }
 | 
			
		||||
      if (*arg == '"' || !*arg) {
 | 
			
		||||
        while (count-- > 0) *d++ = '\\';
 | 
			
		||||
        if (!*arg) break;
 | 
			
		||||
        *d++ = '\\';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    *d++ = *arg++;
 | 
			
		||||
  }
 | 
			
		||||
  *d++ = '"';
 | 
			
		||||
  *d++ = '\0';
 | 
			
		||||
  return q;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void print_error(char *func) {
 | 
			
		||||
  LPVOID buffer;
 | 
			
		||||
  DWORD dw = GetLastError();
 | 
			
		||||
  FormatMessage(
 | 
			
		||||
      FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
 | 
			
		||||
      NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&buffer, 0, NULL);
 | 
			
		||||
  wprintf(L"== %s failed with error %d: %s", func, dw, buffer);
 | 
			
		||||
  LocalFree(buffer);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										39
									
								
								src/utils.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/utils.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
#ifndef TTYD_UTIL_H
 | 
			
		||||
#define TTYD_UTIL_H
 | 
			
		||||
 | 
			
		||||
#define container_of(ptr, type, member)                \
 | 
			
		||||
  ({                                                   \
 | 
			
		||||
    const typeof(((type *)0)->member) *__mptr = (ptr); \
 | 
			
		||||
    (type *)((char *)__mptr - offsetof(type, member)); \
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
// malloc with NULL check
 | 
			
		||||
void *xmalloc(size_t size);
 | 
			
		||||
 | 
			
		||||
// realloc with NULL check
 | 
			
		||||
void *xrealloc(void *p, size_t size);
 | 
			
		||||
 | 
			
		||||
// Convert a string to upper case
 | 
			
		||||
char *uppercase(char *s);
 | 
			
		||||
 | 
			
		||||
// Convert a string to lower case
 | 
			
		||||
char *lowercase(char *s);
 | 
			
		||||
 | 
			
		||||
// Check whether str ends with suffix
 | 
			
		||||
bool endswith(const char *str, const char *suffix);
 | 
			
		||||
 | 
			
		||||
// Get human readable signal string
 | 
			
		||||
int get_sig_name(int sig, char *buf, size_t len);
 | 
			
		||||
 | 
			
		||||
// Get signal code from string like SIGHUP
 | 
			
		||||
int get_sig(const char *sig_name);
 | 
			
		||||
 | 
			
		||||
// Open uri with the default application of system
 | 
			
		||||
int open_uri(char *uri);
 | 
			
		||||
 | 
			
		||||
#ifdef _WIN32
 | 
			
		||||
char *strsep(char **sp, char *sep);
 | 
			
		||||
const char *quote_arg(const char *arg);
 | 
			
		||||
void print_error(char *func);
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // TTYD_UTIL_H
 | 
			
		||||
		Reference in New Issue
	
	Block a user