├── .gitignore ├── GNUmakefile ├── LICENSE ├── README.md ├── addhost.sh ├── ihqu-tmpl.plx ├── ioio.pl ├── mxtx-apu.sh ├── mxtx-cp.sh ├── mxtx-gitxxdiff.pl ├── mxtx-mosh.md ├── mxtx-mosh.pl ├── mxtx-qpf-hack.pl ├── mxtx.el ├── rsh-hook.tmpl ├── src ├── ldpreload-i2usocket.c ├── ldpreload-moshclienthax.c ├── ldpreload-tcp1271conn.c ├── ldpreload-vsfa.c ├── lineread.ch ├── lpktread.ch ├── more-warnings.h ├── mxtx-dgramtunneld.c ├── mxtx-io.c ├── mxtx-lib.c ├── mxtx-lib.h ├── mxtx-rsh.c ├── mxtx-rsh.ch ├── mxtx-rshd.c ├── mxtx-socksproxy.c └── mxtx.c ├── termtower-tmpl.sh ├── tst ├── multi-strace.sh ├── test-disp-in-emacs └── test-strace-wrapper.sh └── version.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build results. Any other noise is visible. 2 | /ldpreload-i2ubind.so 3 | /ldpreload-i2uconnect5.so 4 | /ldpreload-vsfa.so 5 | /ldpreload-moshclienthax.so 6 | /ldpreload-tcp1271conn.so 7 | /libmxtx.a 8 | /mxtx 9 | /mxtx-io 10 | /mxtx-rsh 11 | /mxtx-rshd 12 | /mxtx-socksproxy 13 | mxtx-dgramtunneld 14 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | # 2 | # $ GNUmakefile $ 3 | # 4 | # Author: Tomi Ollila -- too ät iki piste fi 5 | # 6 | # Copyright (c) 2017 Tomi Ollila 7 | # All rights reserved 8 | # 9 | # Created: Wed 16 Aug 2017 21:09:05 EEST too 10 | # Last modified: Fri 29 Apr 2022 16:44:59 +0300 too 11 | 12 | SHELL = /bin/sh 13 | 14 | BIN := mxtx mxtx-io mxtx-rshd mxtx-rsh mxtx-socksproxy mxtx-dgramtunneld 15 | BIN += ldpreload-i2ubind.so ldpreload-i2uconnect5.so 16 | BIN += ldpreload-vsfa.so ldpreload-moshclienthax.so ldpreload-tcp1271conn.so 17 | 18 | # Note: all source file dependencies may not be listed (usually not a problem) 19 | # do `make clean all` at the end of development session... 20 | 21 | all: $(BIN) 22 | 23 | mxtx: src/mxtx.c libmxtx.a $(wildcard .git/index) 24 | sh $< 25 | 26 | mxtx-lib.o: src/mxtx-lib.c src/mxtx-lib.h 27 | sh $< 28 | 29 | libmxtx.a: src/mxtx-lib.c src/mxtx-lib.h 30 | perl -x $< 31 | 32 | mxtx-io: src/mxtx-io.c libmxtx.a 33 | sh $< 34 | 35 | mxtx-rshd: src/mxtx-rshd.c src/lpktread.ch libmxtx.a 36 | sh $< 37 | 38 | mxtx-rsh: src/mxtx-rsh.c src/lpktread.ch libmxtx.a 39 | sh $< 40 | 41 | mxtx-socksproxy: src/mxtx-socksproxy.c libmxtx.a 42 | sh $< 43 | 44 | mxtx-dgramtunneld: src/mxtx-dgramtunneld.c libmxtx.a 45 | sh $< 46 | 47 | ldpreload-i2ubind.so ldpreload-i2uconnect5.so: src/ldpreload-i2usocket.c 48 | sh $< 49 | 50 | ldpreload-%.so: src/ldpreload-%.c 51 | sh $< 52 | 53 | 54 | YES ?= NO 55 | install: $(BIN) 56 | sed '1,/^$@.sh:/d;/^#.#eos/q' GNUmakefile | /bin/sh -s YES=$(YES) $(BIN) 57 | 58 | install.sh: 59 | test -n "$1" || exit 1 # embedded shell script; not to be made directly 60 | die () { exit 1; } 61 | set -euf 62 | export LC_ALL=C LANG=C 63 | echo 64 | if test "$1" = YES=YES 65 | then mmkdir () { test -d "$1" || mkdir -vp "$1"; } 66 | mmkdir $HOME/bin/ 67 | mmkdir $HOME/.local/share/mxtx/ 68 | shift 69 | imsg=true 70 | echo strip "$@" >&2; strip "$@" 71 | xcp () { test ! -d "$3" || set -- "$1" 2 "${3%/}/${1##*/}" 72 | cmp "$1" "$3" >/dev/null 2>&1 && 73 | echo "'$1' and '$3' identical" || 74 | cp -f -v "$1" "$3"; } 75 | echo Copying: 76 | elif test "$1" = YES=DIFF # "undocumented" option... 77 | then imsg=true 78 | xcp () { 79 | test ! -d "$3" || set -- "$1" 2 "${3%/}/${1##*/}" 80 | test -e "$3" || { echo "'$3': no file"; return; } 81 | cmp "$1" "$3" >/dev/null 2>&1 || 82 | diff -u "$3" "$1" || : 83 | } 84 | echo Diffing... 85 | else 86 | imsg=false 87 | xcp () { echo '' "$@"; } 88 | echo Would install: 89 | fi 90 | xcp mxtx as $HOME/bin/.mxtx 91 | xcp ioio.pl to $HOME/bin/ 92 | xcp mxtx.el to $HOME/.local/share/mxtx/ 93 | xcp addhost.sh to $HOME/.local/share/mxtx/ 94 | xcp mxtx-rshd to $HOME/.local/share/mxtx/ 95 | xcp mxtx-io to $HOME/bin/ 96 | xcp mxtx-rsh to $HOME/bin/ 97 | xcp mxtx-cp.sh as $HOME/bin/mxtx-cp 98 | xcp mxtx-apu.sh to $HOME/bin/ 99 | xcp mxtx-mosh.pl as $HOME/bin/mxtx-mosh 100 | xcp mxtx-dgramtunneld to $HOME/.local/share/mxtx/ 101 | xcp mxtx-socksproxy as $HOME/.local/share/mxtx/socksproxy 102 | xcp ldpreload-i2ubind.so to $HOME/.local/share/mxtx/ 103 | xcp ldpreload-i2uconnect5.so to $HOME/.local/share/mxtx/ 104 | xcp ldpreload-vsfa.so to $HOME/.local/share/mxtx/ 105 | xcp ldpreload-moshclienthax.so to $HOME/.local/share/mxtx/ 106 | xcp ldpreload-tcp1271conn.so to $HOME/.local/share/mxtx/ 107 | echo 108 | $imsg || { 109 | echo Enter '' make install YES=YES '' to do so. 110 | echo 111 | } 112 | # #eos 113 | exit 1 # not reached 114 | 115 | # possibly partial uninstall 116 | unin: 117 | cd "$$HOME" && exec rm -f \ 118 | .local/share/mxtx/mxtx[.-]* \ 119 | .local/share/mxtx/ldpreload-* \ 120 | .local/share/mxtx/addhost.sh \ 121 | .local/share/mxtx/socksproxy \ 122 | ./bin/.mxtx bin/mxtx-* bin/ioio.pl 123 | 124 | .PHONY: git-head 125 | git-head: 126 | sed '1,/^$@.sh:/d;/^#.#eos/q' GNUmakefile | /bin/sh -s $@ 127 | 128 | git-head.sh: 129 | test -n "$1" || exit 1 # embedded shell script; not to be made directly 130 | die () { printf %s\\n "$*" >&2; exit 1; } 131 | set -euf 132 | # #test -z "`exec git status --porcelain -uno`" || die changes in working copy 133 | fmt='format:commit %H %h%ntree %T %t%n' 134 | fmt=$fmt'Author: %an <%ae>%nAuthorDate: %aD%n' 135 | fmt=$fmt'Commit: %cn <%ce>%nCommitDate: %cD%n%n% B' 136 | git log --pretty="$fmt" --name-status -1 > "$1" 137 | # #eos 138 | exit 1 # not reached 139 | 140 | .PHONY: dist 141 | dist: #git-head 142 | @test -z "`exec git status --porcelain -uno`" || \ 143 | echo Note: not taking changes from \'dirty\' working tree! 144 | @set -euf; set x `exec git log -1 --pretty='%h %ci'`; s=$$3-g$$2; \ 145 | set -x; exec git archive --prefix=mxtx-$$s/ -o mxtx-$$s.tar.gz HEAD 146 | 147 | # convenience helper to update mxtx over an mxtx link 148 | .PHONY: remote-update 149 | remote-update: 150 | sed '1,/^$@.sh:/d;/^#.#eos/q' GNUmakefile | /bin/sh -s "$(MAKE)" "$D" 151 | 152 | remote-update.sh: 153 | test -n "$1" || exit 1 # embedded shell script; not to be made directly 154 | die () { printf %s\\n "$*" >&2; exit 1; } 155 | set -euf 156 | export LC_ALL=C LANG=C 157 | test -n "$2" || die Usage':' $1 remote-update D=dest 158 | make=$1 D=$2 159 | set x `exec git log -1 --pretty='%h %ci'`; s=$3-g$2; 160 | die () { exit 1; } 161 | set -x 162 | test -f mxtx-$s.tar.gz || die mxtx-$s.tar.gz not made "($make a)" 163 | mxtx-rsh -n "$D" . /bin/true 164 | set +e 165 | mxtx-rsh "$D" . tar zxvf - < mxtx-$s.tar.gz 166 | mxtx-rsh -n "$D" cd mxtx-$s '&&' make install YES=YES 167 | mxtx-rsh -n "$D" . rm -rf mxtx-$s 168 | # #eos 169 | exit 1 # not reached 170 | 171 | clean: 172 | rm -rf $(BIN) mxtx-lib.o libmxtx.a git-head *~ src/*~ _tmp 173 | 174 | .SUFFIXES: 175 | MAKEFLAGS += --no-builtin-rules --warn-undefined-variables 176 | 177 | # Local variables: 178 | # mode: makefile 179 | # End: 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | LICENSE: 2-clause BSD license ("Simplified BSD License"): 3 | 4 | Copyright © 2017, Tomi Ollila 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright 15 | notice, this list of conditions and the following disclaimer in the 16 | documentation and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 22 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ioiomxtx 3 | ======== 4 | 5 | The *ioiomxtx* software set provides `ioio.pl`, simple tool to connect 2 6 | programs together via their stdio (1,0,1,0) file descriptors. This feature 7 | yields many very powerful possibilities. 8 | 9 | The rest of the files here implements software components for multiplexing 10 | data through a software tunnel; this tunnel can be created using `ioio.pl` 11 | to connect one `mxtx` endpoint to another, often utilizing **ssh** on one 12 | (or both!) endpoints. 13 | 14 | In order to use `mxtx` this software needs to be installed on every tunnel 15 | endpoints. 16 | 17 | 18 | tl;dr; 19 | ------ 20 | 21 | ``` 22 | $ git pull --rebase --autostash 23 | $ make install YES=YES 24 | $ rm -f mxtx-20[12]*.tar.gz 25 | $ make dist 26 | $ scp mxtx-20[12]*.tar.gz rhost: 27 | $ ssh rhost 'tar zxf mxtx-20[12]*.tar.gz' 28 | $ ssh rhost 'cd mxtx-20[12]* && exec make install YES=YES' 29 | $ ssh rhost 'rm -rf mxtx-20[12]*' 30 | $ mxtx-apu.sh sshmxtx lnk rhost 31 | $ : on another terminal : 32 | $ mxtx-mosh lnk || mxtx-rsh lnk 33 | ``` 34 | 35 | 36 | ioio 37 | ---- 38 | 39 | `ioio.pl` runs two (2) commands and connects their stdio streams to each other. 40 | The implementation is very simple, effectively just around 40 lines of code. 41 | 42 | The command line usage looks like: 43 | 44 | ``` 45 | $ ioio.pl command1 [args] '///' command2 [args] 46 | ``` 47 | 48 | The string `///` is used to separate the command lines. Note that 49 | `command1` needs to write something into stdout before `command2` is 50 | executed; this is due to that the commands may ask e.g. passwords from 51 | tty before continuing -- this way the requests don't intermix (usually). 52 | 53 | With such a simple program some pretty neat things can be achieved; 54 | usually in companion with `ssh(1)`. 55 | 56 | As an alternative to `///` separator, "specially" formatted first argument 57 | can also be used as a separator (`ioio.pl` contains built-in help for more 58 | information). The following example use this alternative ('.' as a separator): 59 | 60 | ``` 61 | $ ioio.pl . ssh rhost sshfs r: mnt/rhost -o slave . /path/to/sftp-server -R 62 | ``` 63 | 64 | The above makes reverse sshfs mount -- local current directory is mounted 65 | as `mnt/rhost` on remote system, in read-only mode. Note that sftp-server 66 | has capability to access parent directories of the initial path if such 67 | thing would be an issue (e.g. using ptrace or gdb to attach running sshfs...). 68 | 69 | And: 70 | 71 | ``` 72 | $ ioio.pl ssh host1 .mxtx -c /// ssh host2 .mxtx -s 73 | ``` 74 | 75 | Accesses *host2* from *host1* using mxtx (installed as `$HOME/bin/.mxtx`) via 76 | this current host working as intermediate gateway. After ssh negotiation with 77 | *host1* is completed, the handshake message from mxtx (client) is received, 78 | and then connection to *host2* will be initiated. In this case, the order 79 | of commands could be reversed, as the mxtx server would start the handshake 80 | the same way... 81 | 82 | The above example created mxtx link `0` on client host (`host1`) for other 83 | mxtx-* commands to use. Running `.mxtx` without parameters will give good 84 | usage information for other options (better to document there than here). 85 | 86 | 87 | mxtx 88 | ---- 89 | 90 | `mxtx` is software component which creates tunnel between 2 endpoints and 91 | multiplexes traffic between these. Maximum of 250 simultaneous "connections" 92 | can be handled by this software. On one end `mxtx` is started in `client` 93 | mode, and in `server` mode on another. 94 | 95 | `mxtx` client binds unix domain socket where mxtx-aware client software can 96 | connect. Via this socket client software requests a command to be executed on 97 | server. `mxtx` client creates new "connection" and sends request to server, 98 | and bidirectional stdio between mxtx client and the program executed by server 99 | is then transferred (until EOF is received on either end). 100 | 101 | `mxtx` does not handle any flow control -- usually the socketpairs it 102 | uses to communicate with programs can take all the data coming from network 103 | socket fast enough, and if not, it waits a while to get data delivered. 104 | If endpoint is still too slow to read data (e.g. startup time took too much 105 | time), the connection will be dropped. In the case flow control is desired, 106 | the endpoints must have protocol to communicate that (so far I've managed 107 | fine without it). 108 | 109 | (That said, simple `mxtx-rsh {link} git -C {path/to/repo} log | less` can 110 | be used to trigger this "feature" (when log long enough...).) 111 | 112 | Typically `ioio.pl` is used to start `mxtx` endpoints (for the time being)... 113 | 114 | ### build c source and install 115 | 116 | ``` 117 | $ make 118 | $ make install 119 | $ make install YES=YES 120 | ``` 121 | 122 | `mxtx` is installed as `$HOME/bin/.mxtx`, to move it away from e.g. tab 123 | completion (otoh, of course, .m<TAB> completes it). It is basically a 124 | daemon program, launched by user (once) so this feels like a good name for it. 125 | 126 | Rest of user-executable programs are installed with `$HOME/bin/.mxtx-` prefix 127 | and other accombanied files at `$HOME/.local/share/mxtx/`. The command 128 | `make unin` removes most of the installed files (should remove all in 129 | `$HOME/bin/` but leaves `$HOME/.local/share/mxtx/` around). 130 | 131 | Like mentioned before `mxtx` need to be installed on all endpoints tunnels 132 | are to be created. `make dist` can be used to (git-)archive sources for 133 | bootstrap copying. 134 | 135 | ### naming and tab completion 136 | 137 | Naming is hard, and so is tab completion. After trying a few naming options 138 | (to make tab completion easier), I resorted back to (initial) `mxtx` but 139 | wanted `mx` prefix always complete to `mxtx-` -- and finally succeeded 140 | on zsh using old-style *compctl* interface. 141 | 142 | ``` 143 | function comp_cmdxpn { 144 | case $1'|'$2 145 | in mx'|') reply=(mxtx- mxtx--) 146 | ;; *) reply=() 147 | esac 148 | } 149 | compctl -C -K comp_cmdxpn + -c 150 | ``` 151 | 152 | This is good for me, but patches welcome on anything better (or bash support). 153 | 154 | ### short command introduction 155 | 156 | #### mxtx-mosh 157 | 158 | 'Mobile shell'. Tunnels the UDP traffic between mosh-clients and mosh-servers 159 | through an mxtx channel. Works pretty much like normal mosh application. 160 | Requires Mosh to be installed on mxtx endpoints. 161 | 162 | #### mxtx-rsh 163 | 164 | 'Remote' shell. Requests execution of `mxtx-rshd`. Multiplexes stdout, stderr 165 | and return value. Tracks window size (when with tty) and sends WINCH requests. 166 | 167 | #### mxtx-io 168 | 169 | Very simple `ioio` -like functionality. No additional multiplexing -- stderr 170 | of executed command is shown (on terminal) where `mxtx`s were started. 171 | 172 | #### mxtx-cp 173 | 174 | Mxtx copy command. Utilizes `rsync(1)` (subset of rsync options available, some 175 | set on default). There is also `--tar` workaround mode to environments where 176 | rsync(1) is not available (on either communication endpoints). In case there 177 | wasn't even `tar(1)` available, user could resort to `mxtx-io`, `cat(1)` and 178 | shell redirections to copy a file. 179 | 180 | #### socksproxy 181 | 182 | Special utility to tunnel traffic to designated destinations after socks5 183 | communication is completed. Binds to unix domain socket so an *ldpreload* 184 | library is used to make clients able to connect to it. There are commands 185 | `mxtx-apu.sh chromie` and `mxtx-apu.sh ffox` which start special (incognito) 186 | chrome/chromium and firefox (respectively) instance which uses this 187 | socksproxy for connections. 188 | (Note: in case of chromie, one can add `--proxy-bypass-list` command line 189 | option to silence attempt to access some sites.) 190 | 191 | `mxtx-socksproxy` is installed as `$HOME/.local/share/mxtx/socksproxy` 192 | (for now). It takes mxtx *link* names as arguments. When run it reads 193 | ($HOME/.local/share/mxtx/)`hosts-to-proxy` files on link 194 | targets to see what hosts are connectable behind every particular link 195 | ('', '/' and '.' resolve to local system -- for those connections that can 196 | be made directly by socksproxy). The tool `./addhost.sh` (installed to the 197 | same .local/share/mxtx/ directory) can be used to ease adding hosts to the 198 | `hosts-to-proxy` file. 199 | 200 | #### mxtx-apu.sh 201 | 202 | Mxtx Acute Program Usage, is a wrapper command to make some ordinary system 203 | commands use mxtx tunnels for their operations. The tool provides self-help 204 | when executed without arguments. 205 | 206 | #### mxtx.el 207 | 208 | An emacs lisp file mainly to add mxtx tramp support. `mxtx-apu.sh` has `emacs` 209 | command to ease using this file. 210 | 211 | #### ihqu-tmpl.plx 212 | 213 | Index.Html Quite Useful -- `socksproxy` request to load 214 | `$HOME/.local/share/mxtx/socksproxy/index.html` (behind link) when it is 215 | asked to forward request to `http://index-{link}.html` page. 216 | This file can be used to create such an `index.html` file (look into it). 217 | 218 | #### addhost.sh 219 | 220 | Resolves ipv4 address of a given hostname and writes results to 221 | `$HOME/.local/share/mxtx/hosts-to-proxy` (used by *socksproxy*). 222 | 223 | #### termtower-tmpl.sh 224 | 225 | `termtower-tmpl.sh`, as named as template, when executed as is, moves current 226 | graphical terminal to a new location and opens 2 more terminal windows beneath 227 | it (uxrvt, xterm (or mintty)). These terminals can be used e.g. to open `mxtx` 228 | tunnels to several hosts. Copy and edit to suit your needs... 229 | -------------------------------------------------------------------------------- /addhost.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # $ addhost.sh -- add host to hosts-to-proxy for socksproxy to use $ 4 | # 5 | # Author: Tomi Ollila -- too ät iki piste fi 6 | # 7 | # Copyright (c) 2017 Tomi Ollila 8 | # All rights reserved 9 | # 10 | # Created: Thu 24 Aug 2017 22:13:39 EEST too 11 | # Last modified: Mon 08 Apr 2019 20:03:21 +0300 too 12 | 13 | case ${BASH_VERSION-} in *.*) set -o posix; shopt -s xpg_echo; esac 14 | case ${ZSH_VERSION-} in *.*) emulate ksh; esac 15 | 16 | set -euf 17 | #set -x 18 | 19 | LANG=C LC_ALL=C export LANG LC_ALL; unset LANGUAGE 20 | 21 | die () { printf '%s\n' "$*"; exit 1; } >&2 22 | 23 | test $# = 1 || die "Usage: $0 host" 24 | 25 | cd $HOME/.local/share/mxtx 26 | echo Working directory: $PWD/ 27 | if grep "$1" hosts-to-proxy 2>/dev/null 28 | then die "'$1' is already in 'hosts-to-proxy file" 29 | fi 30 | # ipv4 only for now... grab first ipv4 address available 31 | am=[1-9][0-9]* 32 | host=`host -t A "$1" | sed "s/.* \($am\.$am\.$am\.$am\).*/\1/;tx;d;:x;q"` 33 | test "$host" || die "Could not figure address to host '$1'" 34 | echo "$1 $host" >> hosts-to-proxy 35 | echo "Added $1 $host to hosts-to-proxy" 36 | echo : Hint'; ' pkill -USR1 socksproxy 37 | 38 | # Local variables: 39 | # mode: shell-script 40 | # sh-basic-offset: 8 41 | # sh-indentation: 8 42 | # tab-width: 8 43 | # End: 44 | # vi: set sw=8 ts=8 45 | -------------------------------------------------------------------------------- /ihqu-tmpl.plx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # -*- mode: cperl; cperl-indent-level: 4 -*- 3 | 4 | # copy this file as index-src.pl, replace your links below in $USER_LINKS 5 | # and tune $TAIL (perhaps edit $HEAD), and then do $ perl index-src.pl 6 | 7 | use 5.8.1; 8 | use strict; 9 | use warnings; 10 | 11 | # note: all caps just to emphasize the parts most useful to be edited 12 | 13 | my $USER_LINKS = <<'EOF'; 14 | 15 | http://tbl1col1.example.com/ tbl1 col1 16 | http://tbl1col2.example.com/ tbl1 col2 17 | http://tbl1col3.example.com/ tbl1 col3 18 | 19 | http://tbl2col1.example.com/ tbl2 col-1 20 | http://tbl2col2.example.com/ tbl2 col-2 21 | 22 | http://tbl3col1.example.com/ tbl#3 col 1 23 | 24 | = ahdr: 25 | http://tbl4col2.example.com/ tbl4 col 2 26 | 27 | 28 | EOF 29 | 30 | my $TAIL = <<"EOF"; 31 | 32 | 33 | 41 |
42 | 43 | 47 | 48 | 52 | 53 | |