├── version.h ├── packaging ├── .gitignore ├── scripts │ ├── before-install │ ├── before-remove │ └── after-install ├── etc │ └── owntracks-cli-publisher.env ├── dockerfiles │ ├── Dockerfile.builder │ ├── Dockerfile.debian.baseline │ └── Dockerfile.centos.baseline ├── systemd │ └── owntracks-cli-publisher.service ├── builder │ └── fpm-package ├── README.rst └── Makefile ├── .github └── FUNDING.yml ├── contrib ├── fake │ ├── .gitignore │ ├── run-fakegps.sh │ ├── paris.png │ ├── run-ocli.sh │ ├── readme.txt │ └── paris.nmea └── platform ├── assets ├── img_9643.jpg ├── jmbp-5862.png └── owntrackscli192.png ├── .gitignore ├── owntracks-cli-publisher.pdf ├── Makefile ├── CHANGES.md ├── json.h ├── LICENSE ├── owntracks-cli-publisher.1 ├── README.md ├── utstring.h ├── utarray.h ├── owntracks-cli-publisher.c └── json.c /version.h: -------------------------------------------------------------------------------- 1 | #define VERSION "0.9.0" 2 | -------------------------------------------------------------------------------- /packaging/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /dist 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: owntracks.org 2 | -------------------------------------------------------------------------------- /contrib/fake/.gitignore: -------------------------------------------------------------------------------- 1 | !run-fakegps.sh 2 | !run-ocli.sh 3 | -------------------------------------------------------------------------------- /assets/img_9643.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owntracks/ocli/HEAD/assets/img_9643.jpg -------------------------------------------------------------------------------- /assets/jmbp-5862.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owntracks/ocli/HEAD/assets/jmbp-5862.png -------------------------------------------------------------------------------- /contrib/fake/run-fakegps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gpsfake -c 1 -P 5000 \ 4 | paris.nmea 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | owntracks-cli-publisher 3 | *.dSYM 4 | .DS_Store 5 | parms/ 6 | run*.sh 7 | -------------------------------------------------------------------------------- /contrib/fake/paris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owntracks/ocli/HEAD/contrib/fake/paris.png -------------------------------------------------------------------------------- /assets/owntrackscli192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owntracks/ocli/HEAD/assets/owntrackscli192.png -------------------------------------------------------------------------------- /owntracks-cli-publisher.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owntracks/ocli/HEAD/owntracks-cli-publisher.pdf -------------------------------------------------------------------------------- /packaging/scripts/before-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Add user 4 | useradd --create-home --shell /bin/bash owntracks || true 5 | -------------------------------------------------------------------------------- /packaging/scripts/before-remove: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Stop and disable the service. 4 | systemctl stop owntracks-cli-publisher 5 | systemctl disable owntracks-cli-publisher 6 | -------------------------------------------------------------------------------- /packaging/etc/owntracks-cli-publisher.env: -------------------------------------------------------------------------------- 1 | # ================================================== 2 | # Custom startup options for owntracks-cli-publisher 3 | # ================================================== 4 | 5 | # this file is read by the systemd unit 6 | 7 | # fixlog="/tmp/owntracks-fix.log" 8 | # BASE_TOPIC="owntracks/someuser/somedevice" 9 | MQTT_HOST="localhost" 10 | MQTT_PORT=1883 11 | GPSD_HOST="localhost" 12 | GPSD_PORT="2947" 13 | OCLI_TID="OC" 14 | -------------------------------------------------------------------------------- /packaging/scripts/after-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | systemctl reenable owntracks-cli-publisher 4 | 5 | # Don't autostart. User will have to configure it first. 6 | #systemctl restart owntracks-cli-publisher 7 | 8 | echo 9 | echo "1. Please adjust configuration: /etc/default/owntracks-cli-publisher.env" 10 | echo "2. Start daemon: systemctl start owntracks-cli-publisher" 11 | echo "3. Watch logfile: /tmp/owntracks-fix.log" 12 | echo "4. Subscribe to MQTT broker and see what's going on there" 13 | echo 14 | -------------------------------------------------------------------------------- /contrib/fake/run-ocli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # use hashed hostname as last element of topic branch 4 | # to try and anonymize accesses. 5 | 6 | h=`hostname` 7 | t=`md5 -s $h | awk '{print $4}'` 8 | 9 | export MQTT_HOST=test.mosquitto.org 10 | export MQTT_PORT=1883 11 | export GPSD_PORT=5000 12 | export OCLI_DISPLACEMENT=0 13 | export OCLI_INTERVAL=0 14 | export BASE_TOPIC="ouch/fake/${t}" 15 | 16 | cat < $(OCLI).tmp_ && pstopdf -i $(OCLI).tmp_ -o $(OCLI).pdf && rm -f $(OCLI).tmp_ 20 | 21 | install: $(OCLI) $(OCLI).1 22 | install -d $(DESTDIR)$(BINDIR) 23 | install -m755 $(OCLI) $(DESTDIR)$(BINDIR)/$(OCLI) 24 | install -d $(DESTDIR)$(MANDIR)/man1 25 | install -m644 $(OCLI).1 $(DESTDIR)$(MANDIR)/man1/$(OCLI).1 26 | 27 | clean: 28 | rm -f *.o 29 | 30 | clobber: clean 31 | rm -f $(OCLI) 32 | -------------------------------------------------------------------------------- /contrib/platform: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # platform.sh 4 | # Copyright (C) 2019 Jan-Piet Mens 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | 12 | # Linux 13 | if [ -f /etc/os-release ]; then 14 | . /etc/os-release 15 | echo "${ID}${VERSION_ID}" 16 | exit 17 | fi 18 | 19 | # macOS 20 | if [ -x /usr/bin/sw_vers ]; then 21 | echo "macOS$(/usr/bin/sw_vers -productVersion | awk -F. '{print $1}')" 22 | exit 23 | fi 24 | 25 | # FreeBSD 26 | 27 | if [ -x /bin/freebsd-version ]; then 28 | echo "FreeBSD$(/bin/freebsd-version | sed 's/-.*//')" 29 | exit 30 | fi 31 | 32 | # OpenBSD, ... 33 | if [ -x /usr/bin/uname ]; then 34 | /usr/bin/uname 35 | else 36 | /bin/uname 37 | fi 38 | 39 | -------------------------------------------------------------------------------- /packaging/systemd/owntracks-cli-publisher.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OwnTracks publisher daemon 3 | Documentation=https://github.com/owntracks/ocli 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | EnvironmentFile=/etc/default/owntracks-cli-publisher.env 9 | ExecStart=/usr/local/bin/ocli 10 | Restart=always 11 | RestartSec=60 12 | User=owntracks 13 | Group=owntracks 14 | # Harden system access 15 | # 16 | # if you need to turn off some protection 17 | # then create file 18 | # /etc/systemd/system/owntracks-cli-publisher.service.d/harden.conf 19 | # 20 | # For example turn off ProtectHome: 21 | # [Service] 22 | # ProtectHome=false 23 | # 24 | ProtectSystem=full 25 | ProtectHome=true 26 | ProtectHostname=true 27 | ProtectClock=true 28 | ProtectKernelTunables=true 29 | ProtectKernelModules=true 30 | ProtectKernelLogs=true 31 | ProtectControlGroups=true 32 | RestrictRealtime=true 33 | 34 | [Install] 35 | WantedBy=multi-user.target 36 | Alias=owntracks-cli-publisher.service 37 | -------------------------------------------------------------------------------- /packaging/dockerfiles/Dockerfile.debian.baseline: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE 2 | 3 | 4 | # ========================= 5 | # Derive from superbaseline 6 | # ========================= 7 | FROM ${BASE_IMAGE} AS debian-base 8 | RUN apt-get update && apt-get upgrade 9 | 10 | 11 | # =========== 12 | # Build tools 13 | # =========== 14 | FROM debian-base AS debian-build 15 | 16 | # Build foundation and header files 17 | RUN apt-get install --yes --no-install-recommends \ 18 | apt-utils git nano \ 19 | build-essential pkg-config libssl-dev 20 | 21 | 22 | # =============== 23 | # Packaging tools 24 | # =============== 25 | FROM debian-build AS debian-fpm 26 | 27 | # FPM 28 | RUN echo && echo "Installing fpm. This might take a while." && echo 29 | RUN apt-get install --yes --no-install-recommends \ 30 | ruby ruby-dev && \ 31 | gem install fpm --version 1.11.0 32 | 33 | 34 | # =========================== 35 | # Customize build environment 36 | # =========================== 37 | FROM debian-fpm 38 | 39 | RUN apt-get install --yes --no-install-recommends libgps-dev libmosquitto-dev 40 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # ocli changelog 2 | 3 | ## 2020-02-15 0.9.1 4 | - Add package building for CentOS 6,7,8 5 | - Rename package to "owntracks-cli-publisher" 6 | - Use version tag from "version.h". 7 | - silence switch is honoured 8 | 9 | ## 2020-01-12 0.8.0 10 | - add support for pledge(2) and unveil(2) in OpenBSD 11 | - Improve fake gps data for Paris, thanks to @ckrey 12 | - Clarify subscription 13 | - Add version, adapt CFLAGS, and -s and -v 14 | - Add infrastructure for Docker-based packaging (#4) 15 | - Switch to Semantic Versioning 16 | - Decrease default publish interval to 1s 17 | - Add fake data generator 18 | - Replace `bash` markdown code blocks with `console`; by @linusg 19 | - Initial import from jps2m 20 | - Add support for file params 21 | - Support for reading from executable params 22 | - Add -I and -L for *BSD 23 | - Bring to GPSD API level 8 (for OpenBSD) 24 | - Started on documentation 25 | - Carry mosq pointer in udata 26 | - Put all configurables in udata structure 27 | - Document parms 28 | - Rename minsecs and minmove consistently 29 | - Document building; lol centos8 30 | - Add some support for remoteConfig 31 | - Make clientid configurable 32 | - OwnTracks on macOS 33 | - Add license file 34 | - Change executable uname to contrib/platform 35 | - Thank you, @ckrey, for the logo! 36 | - Change default MQTT clientid 37 | - Dump crashes if tid unset; reported by @ckrey 38 | - reportLocation should print now() in tst; reported by @ckrey 39 | - Publish all locations with retain; reported by @ckrey 40 | - Selectively use retain; cleanup 41 | - Subscribe on connect so we subscribe after auto-reconnect 42 | - [platform] Be more specific for FreeBSD 43 | - Add support for TLS; general cleanup; closes #1 44 | -------------------------------------------------------------------------------- /packaging/builder/fpm-package: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Packaging the "owntracks-cli-publisher" using "fpm". 4 | ## 5 | # Synopsis:: 6 | # 7 | # fpm-package owntracks-cli-publisher buster deb 0.7.0 8 | # fpm-package owntracks-cli-publisher centos8 rpm 0.7.0 9 | # 10 | 11 | NAME=$1 12 | DISTRIBUTION=$2 13 | PKGTYPE=$3 14 | VERSION=$4 15 | 16 | echo "Building package ${NAME}-${VERSION} for ${DISTRIBUTION}" 17 | 18 | mkdir ./dist 19 | 20 | # Build Debian package 21 | fpm \ 22 | -s dir -t ${PKGTYPE} \ 23 | \ 24 | --name ${NAME} \ 25 | --version ${VERSION} \ 26 | --iteration 1~${DISTRIBUTION} \ 27 | \ 28 | --deb-user owntracks \ 29 | --deb-group owntracks \ 30 | --no-deb-use-file-permissions \ 31 | --depends "libmosquitto1, libgps23" \ 32 | --provides "${NAME}" \ 33 | --maintainer "jpmens@gmail.com" \ 34 | --license "GPL 2" \ 35 | --deb-changelog CHANGES.md \ 36 | --deb-meta-file README.md \ 37 | --description "OwnTracks CLI publisher" \ 38 | --url "https://github.com/owntracks/ocli" \ 39 | \ 40 | --package ./dist/ \ 41 | --deb-default ./packaging/etc/owntracks-cli-publisher.env \ 42 | --before-install ./packaging/scripts/before-install \ 43 | --after-install ./packaging/scripts/after-install \ 44 | --before-remove ./packaging/scripts/before-remove \ 45 | --verbose \ 46 | --force \ 47 | ./owntracks-cli-publisher=/usr/local/bin/owntracks-cli-publisher \ 48 | ./owntracks-cli-publisher.1=/usr/local/man/man1/owntracks-cli-publisher.1 \ 49 | ./packaging/systemd/owntracks-cli-publisher.service=/usr/lib/systemd/system/owntracks-cli-publisher.service 50 | 51 | 52 | # Optionally 53 | # --debug \ 54 | 55 | 56 | # Might use again when building from feature branches or other references 57 | #--deb-field 'Branch: $(branch) Commit: $(commit)' \ 58 | #version := $(shell python setup.py --version) 59 | 60 | 61 | 62 | # ------------------- 63 | # Development options 64 | # ------------------- 65 | 66 | # On your fingertips (enable on demand) 67 | 68 | 69 | # general debugging 70 | # --debug \ 71 | 72 | # don't delete working directory (to introspect the cruft in case something went wrong) 73 | # --debug-workspace \ 74 | 75 | # we don't prefix, instead use the convenient mapping syntax {source}={target} 76 | # --prefix /opt/foobar \ 77 | 78 | # we don't set any architecture, let the package builder do it 79 | # --architecture noarch \ 80 | 81 | # there are currently just --deb-init and --deb-upstart options for designating an init- or upstart script 82 | # we already use systemd 83 | 84 | # Add FILEPATH as /etc/default configuration 85 | # --deb-default abc \ 86 | 87 | # amend the shebang of scripts 88 | # --python-scripts-executable '/usr/bin/env python' \ 89 | 90 | # Add custom fields to DEBIAN/control file 91 | # --deb-field 'Branch: master Commit: deadbeef' \ 92 | -------------------------------------------------------------------------------- /packaging/README.rst: -------------------------------------------------------------------------------- 1 | ############################################## 2 | Build, package and publish owntracks-cli-publisher 3 | ############################################## 4 | 5 | .. highlight:: bash 6 | 7 | 8 | ************ 9 | Introduction 10 | ************ 11 | This outlines the Makefile targets of a convenient 12 | packaging subsystem for owntracks-cli-publisher. 13 | 14 | Currently, this is focused on Debian and CentOS packages 15 | but might be extended in the future. 16 | 17 | Three steps are required to successfully build packages. 18 | 19 | 1. Setup: 20 | Create appropriate baseline Docker images for the 21 | designated operating system. 22 | 23 | 2. Build: 24 | Build package using the respective baseline images. 25 | 26 | 3. Publish: 27 | Upload package file to GitHub release page. 28 | 29 | 30 | ***** 31 | Usage 32 | ***** 33 | First, go to the "packaging" directory:: 34 | 35 | cd packaging 36 | 37 | 38 | ***** 39 | Setup 40 | ***** 41 | Prepare baseline images for ``amd64`` architecture:: 42 | 43 | # Debian stretch amd64 44 | make build-debian-baseline arch=amd64 dist=stretch version=0.1.0 45 | 46 | # Debian buster amd64 47 | make build-debian-baseline arch=amd64 dist=buster version=0.1.0 48 | 49 | # CentOS 7 amd64 50 | make build-centos-baseline arch=amd64 dist=7 version=0.1.0 51 | 52 | # CentOS 8 amd64 53 | make build-centos-baseline arch=amd64 dist=8 version=0.1.0 54 | 55 | 56 | Prepare baseline images for ``armhf`` architecture:: 57 | 58 | # Debian stretch armhf 59 | make build-debian-baseline arch=armv7hf dist=stretch version=0.1.0 60 | 61 | # TODO: make build-centos-baseline arch=arm32v7 dist=7 version=0.1.0 62 | # TODO: make build-centos-baseline arch=arm64v8 dist=7 version=0.1.0 63 | 64 | 65 | ***** 66 | Build 67 | ***** 68 | Build ``owntracks-cli-publisher`` package for ``amd64`` architecture:: 69 | 70 | # Debian stretch amd64 71 | make debian-package arch=amd64 dist=stretch pkgtype=deb 72 | 73 | # Debian buster amd64 74 | make debian-package arch=amd64 dist=buster pkgtype=deb 75 | 76 | # CentOS 7 amd64 77 | make centos-package arch=amd64 dist=centos7 pkgtype=rpm 78 | 79 | # CentOS 8 amd64 80 | make centos-package arch=amd64 dist=centos8 pkgtype=rpm 81 | 82 | 83 | Build ``owntracks-cli-publisher`` package for ``armhf`` architecture:: 84 | 85 | # Debian stretch armhf 86 | make debian-package arch=armhf dist=stretch pkgtype=deb 87 | 88 | 89 | ******* 90 | Publish 91 | ******* 92 | Publish package to GitHub:: 93 | 94 | # Prepare 95 | export GITHUB_TOKEN=642ff7c47b1697a79ab7f105e1d79f054d0bbeef 96 | 97 | # Debian buster amd64 98 | make publish-package arch=amd64 dist=buster 99 | 100 | # Debian stretch armhf 101 | make publish-package arch=armhf dist=stretch 102 | 103 | 104 | ***** 105 | Notes 106 | ***** 107 | These Docker images are used for creating the baseline images: 108 | 109 | - https://hub.docker.com/r/amd64/debian (todo) 110 | - https://hub.docker.com/r/arm32v7/debian (todo) 111 | - https://hub.docker.com/r/arm64v8/debian (todo) 112 | 113 | - https://hub.docker.com/r/amd64/centos 114 | - https://hub.docker.com/r/arm32v7/centos 115 | - https://hub.docker.com/r/arm64v8/centos 116 | -------------------------------------------------------------------------------- /packaging/dockerfiles/Dockerfile.centos.baseline: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE 2 | 3 | 4 | # ========================= 5 | # Derive from superbaseline 6 | # ========================= 7 | FROM ${BASE_IMAGE} AS centos-base 8 | 9 | RUN yum install -y deltarpm; exit 0 10 | RUN yum update -y 11 | 12 | 13 | # =========== 14 | # Build tools 15 | # =========== 16 | FROM centos-base AS centos-build 17 | 18 | # Build foundation and header files 19 | RUN yum install -y \ 20 | gcc gcc-c++ make pkgconfig 21 | 22 | 23 | # =============== 24 | # Packaging tools 25 | # =============== 26 | FROM centos-build AS centos-fpm 27 | 28 | # FPM 29 | RUN yum install -y \ 30 | ruby ruby-devel rubygems rpm-build 31 | 32 | RUN echo && echo "Installing fpm. This might take a while." && echo 33 | 34 | # CentOS 6: Ruby is too old, so install "fpm" within "rvm" environment. 35 | # https://github.com/jordansissel/fpm/issues/1192#issuecomment-466385257 36 | RUN \ 37 | grep 'release 6' /etc/centos-release || exit 0 && ( \ 38 | yum install -y curl libyaml && \ 39 | curl -sSL https://rvm.io/pkuczynski.asc | gpg2 --import - && \ 40 | curl -L get.rvm.io | bash -s stable && \ 41 | # source /etc/profile.d/rvm.sh \ 42 | export rvm=/usr/local/rvm/bin/rvm && \ 43 | $rvm reload && \ 44 | $rvm install 2.3.1 && \ 45 | $rvm all do gem install fpm --version 1.11.0 && \ 46 | ln -s /usr/local/rvm/gems/ruby-2.3.1/wrappers/fpm /usr/local/bin/fpm \ 47 | ) 48 | 49 | # CentOS 7,8 50 | RUN \ 51 | grep -v 'release 6' /etc/centos-release || exit 0 && ( \ 52 | gem install fpm --version 1.11.0 \ 53 | ) 54 | 55 | 56 | # ========================= 57 | # Prepare build environment 58 | # ========================= 59 | FROM centos-fpm 60 | 61 | RUN yum install -y openssl-devel libuuid-devel wget 62 | 63 | # Install PUIAS and OKey repositories in order 64 | # to install "scons" for building "gpsd". 65 | RUN \ 66 | grep 'release 6' /etc/centos-release || exit 0 && ( \ 67 | wget https://download-ib01.fedoraproject.org/pub/epel/6/x86_64/Packages/e/epel-release-6-8.noarch.rpm && \ 68 | wget http://repo.okay.com.mx/centos/6/x86_64/release//okay-release-1-3.el6.noarch.rpm && \ 69 | rpm -i *.rpm && \ 70 | yum install -y python-devel scons \ 71 | ) 72 | 73 | RUN \ 74 | grep 'release 7' /etc/centos-release || exit 0 && ( \ 75 | wget https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/Packages/e/epel-release-7-12.noarch.rpm && \ 76 | wget http://repo.okay.com.mx/centos/7/x86_64/release//okay-release-1-3.el7.noarch.rpm && \ 77 | rpm -i *.rpm && \ 78 | yum install -y python-devel scons \ 79 | ) 80 | 81 | RUN \ 82 | grep 'release 8' /etc/centos-release || exit 0 && ( \ 83 | wget https://download-ib01.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/e/epel-release-8-8.el8.noarch.rpm && \ 84 | wget http://repo.okay.com.mx/centos/8/x86_64/release//okay-release-1-3.el8.noarch.rpm && \ 85 | rpm -i *.rpm && \ 86 | yum install -y python3-devel scons \ 87 | ) 88 | 89 | 90 | # Install "gpsd". 91 | RUN \ 92 | wget https://gitlab.com/gpsd/gpsd/-/archive/release-3.19/gpsd-release-3.19.tar.gz && \ 93 | tar -xzf gpsd-release-3.19.tar.gz && \ 94 | cd gpsd-release-3.19 && \ 95 | scons && scons install 96 | 97 | # Install "mosquitto". 98 | RUN \ 99 | wget https://mosquitto.org/files/source/mosquitto-1.5.9.tar.gz && \ 100 | tar -xzf mosquitto-1.5.9.tar.gz && \ 101 | cd mosquitto-1.5.9 && \ 102 | make && make install 103 | -------------------------------------------------------------------------------- /json.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | #ifndef CCAN_JSON_H 25 | #define CCAN_JSON_H 26 | 27 | #include 28 | #include 29 | 30 | typedef enum { 31 | JSON_NULL, 32 | JSON_BOOL, 33 | JSON_STRING, 34 | JSON_NUMBER, 35 | JSON_ARRAY, 36 | JSON_OBJECT, 37 | } JsonTag; 38 | 39 | typedef struct JsonNode JsonNode; 40 | 41 | struct JsonNode 42 | { 43 | /* only if parent is an object or array (NULL otherwise) */ 44 | JsonNode *parent; 45 | JsonNode *prev, *next; 46 | 47 | /* only if parent is an object (NULL otherwise) */ 48 | char *key; /* Must be valid UTF-8. */ 49 | 50 | JsonTag tag; 51 | union { 52 | /* JSON_BOOL */ 53 | bool bool_; 54 | 55 | /* JSON_STRING */ 56 | char *string_; /* Must be valid UTF-8. */ 57 | 58 | /* JSON_NUMBER */ 59 | double number_; 60 | 61 | /* JSON_ARRAY */ 62 | /* JSON_OBJECT */ 63 | struct { 64 | JsonNode *head, *tail; 65 | } children; 66 | }; 67 | }; 68 | 69 | /*** Encoding, decoding, and validation ***/ 70 | 71 | JsonNode *json_decode (const char *json); 72 | char *json_encode (const JsonNode *node); 73 | char *json_encode_string (const char *str); 74 | char *json_stringify (const JsonNode *node, const char *space); 75 | void json_delete (JsonNode *node); 76 | 77 | bool json_validate (const char *json); 78 | 79 | /*** Lookup and traversal ***/ 80 | 81 | JsonNode *json_find_element (JsonNode *array, int index); 82 | JsonNode *json_find_member (JsonNode *object, const char *key); 83 | 84 | JsonNode *json_first_child (const JsonNode *node); 85 | 86 | #define json_foreach(i, object_or_array) \ 87 | for ((i) = json_first_child(object_or_array); \ 88 | (i) != NULL; \ 89 | (i) = (i)->next) 90 | 91 | /*** Construction and manipulation ***/ 92 | 93 | JsonNode *json_mknull(void); 94 | JsonNode *json_mkbool(bool b); 95 | JsonNode *json_mkstring(const char *s); 96 | JsonNode *json_mknumber(double n); 97 | JsonNode *json_mkarray(void); 98 | JsonNode *json_mkobject(void); 99 | 100 | void json_append_element(JsonNode *array, JsonNode *element); 101 | void json_prepend_element(JsonNode *array, JsonNode *element); 102 | void json_append_member(JsonNode *object, const char *key, JsonNode *value); 103 | void json_prepend_member(JsonNode *object, const char *key, JsonNode *value); 104 | 105 | void json_remove_from_parent(JsonNode *node); 106 | 107 | /*** Debugging ***/ 108 | 109 | /* 110 | * Look for structure and encoding problems in a JsonNode or its descendents. 111 | * 112 | * If a problem is detected, return false, writing a description of the problem 113 | * to errmsg (unless errmsg is NULL). 114 | */ 115 | bool json_check(const JsonNode *node, char errmsg[256]); 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | OwnTracks CLI 2 | Copyright (C) 2016-2020 Jan-Piet Mens 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU General Public License 6 | as published by the Free Software Foundation; either version 2 7 | of the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | --------------------------------------------------------------------------- 19 | ## utarray.h 20 | 21 | /* 22 | Copyright (c) 2008-2014, Troy D. Hanson http://troydhanson.github.com/uthash/ 23 | All rights reserved. 24 | 25 | Redistribution and use in source and binary forms, with or without 26 | modification, are permitted provided that the following conditions are met: 27 | 28 | * Redistributions of source code must retain the above copyright 29 | notice, this list of conditions and the following disclaimer. 30 | 31 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 32 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 33 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 34 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 35 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 36 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 37 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 38 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 39 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 40 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 41 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 | */ 43 | 44 | --------------------------------------------------------------------------- 45 | ## utstring.h 46 | 47 | /* 48 | Copyright (c) 2008-2014, Troy D. Hanson http://troydhanson.github.com/uthash/ 49 | All rights reserved. 50 | 51 | Redistribution and use in source and binary forms, with or without 52 | modification, are permitted provided that the following conditions are met: 53 | 54 | * Redistributions of source code must retain the above copyright 55 | notice, this list of conditions and the following disclaimer. 56 | 57 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 58 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 59 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 60 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 61 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 62 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 63 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 64 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 65 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 66 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 67 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 68 | */ 69 | 70 | --------------------------------------------------------------------------- 71 | ## json.[ch] 72 | 73 | /* 74 | Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) 75 | All rights reserved. 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy 78 | of this software and associated documentation files (the "Software"), to deal 79 | in the Software without restriction, including without limitation the rights 80 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 81 | copies of the Software, and to permit persons to whom the Software is 82 | furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in 85 | all copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 90 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 92 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 93 | THE SOFTWARE. 94 | */ 95 | 96 | --------------------------------------------------------------------------- 97 | 98 | 99 | -------------------------------------------------------------------------------- /owntracks-cli-publisher.1: -------------------------------------------------------------------------------- 1 | .\" ocli.1 Copyright (C) 2016-2020 Jan-Piet Mens 2 | .\" === 3 | .TH ocli 1 "January 2020" "jpmens" "GPS/MQTT utilities" 4 | .\"----------------------------------------------------------- 5 | .SH NAME 6 | ocli \- OwnTracks command-line interface GPS publisher 7 | .\"----------------------------------------------------------- 8 | .SH SYNOPSIS 9 | .B ocli 10 | [ 11 | -v 12 | ] 13 | [ 14 | .IR parameter-file ... ] 15 | .\"----------------------------------------------------------- 16 | .SH DESCRIPTION 17 | .B ocli 18 | is the OwnTracks command line interface publisher, a small utility which connects to 19 | .B gpsd 20 | (the interface daemon for GPS receivers) 21 | and publishes position information in OwnTracks JSON format to an MQTT broker in order 22 | for compatible software to process location data. 23 | .B ocli 24 | reads GPS data from an installed 25 | .B gpsd 26 | daemon, and as soon as it has a fix publishes an OwnTracks payload. 27 | .B ocli 28 | will subsequently publish a message every \fCOCLI_INTERVAL\fR seconds or when it detects it has moved \fCOCLI_DISPLACEMENT\fR meters. 29 | .PP 30 | .B ocli 31 | operates with a number of defaults which you can override using environment variables. 32 | .SS Parameter files 33 | Any number of path names can be passed as 34 | .I parameter-file 35 | arguments to 36 | .B ocli . 37 | A single line is read from each of the files and added to the OwnTracks JSON payload as elements with the key name of base name of 38 | .IR parameter-file . 39 | If the file is executable, the line is read from its standard output. 40 | .sp 41 | .nf 42 | .in 1i 43 | .ft CW 44 | echo 27.2 > temperature 45 | ocli temperature /usr/bin/uname 46 | .ft 47 | .in 48 | .fi 49 | .sp 50 | will produce a payload which might look like 51 | .sp 52 | .nf 53 | .in 1i 54 | .ft CW 55 | { 56 | "_type": "location", 57 | "tst": 1577654651, 58 | "lat": 48.856826, 59 | 60 | "temperature" : "27.2", 61 | "uname": "FreeBSD" 62 | } 63 | .ft 64 | .in 65 | .fi 66 | .PP 67 | Keys obtained from 68 | .IR parameter-file s 69 | must not overwrite JSON element names define by 70 | .BR ocli , 71 | so, for example, a file called \fClat\fR will be silently ignored as it would clobber the JSON 72 | \fClatitude\fR element. 73 | .SS Control 74 | It is possible to control 75 | .B ocli 76 | using a subset of OwnTrack's \fCcmd\fR commands. 77 | .sp 78 | .nf 79 | .in 1i 80 | .ft CW 81 | t=owntracks/jpm/tiggr/cmd 82 | mosquitto_pub -t $t -m "$(jo _type=cmd action=reportLocation)" 83 | .fi 84 | .in 85 | .ft 86 | .sp 87 | The following commands are currently implemented: 88 | .IP reportLocation 1i 89 | causes 90 | .B ocli 91 | to publish its current location (providing 92 | .B gpsd 93 | has a fix). 94 | .B ocli 95 | sets \fCt:m\fR in the JSON to indicate the publish was manually requested. 96 | .IP dump 1i 97 | causes the program to publish its internal configuration to the topic \fC\fIbasetopic\fR\fC/dump\fR as a \fC_type: configuration\fR message. 98 | .IP setConfiguration 99 | permits setting some of 100 | .BR ocli 's 101 | internal values (\fClocatorInterval\fR and \fClocatorDisplacement\fR). 102 | Note that these do not persist a restart. 103 | .\"----------------------------------------------------------- 104 | .SH OPTIONS 105 | .IP -v 1i 106 | Show version. 107 | .IP -s 1i 108 | By default, 109 | .B ocli 110 | prints informational messages to 111 | .IR stdout . 112 | This option silences those. 113 | .\"----------------------------------------------------------- 114 | .SH ENVIRONMENT 115 | .B ocli 116 | is operates with a number of compiled-in defaults which can be overridden using environment variables: 117 | .IP \fCBASE_TOPIC\fR 1i 118 | The MQTT base topic defaults to \fCowntracks/\fIusername\fR\fC/\fIhostname\fR, where 119 | .I username 120 | is the name of the logged in user, and 121 | .I hostname 122 | the short host name. 123 | .IP \fCMQTT_HOST\fR 1i 124 | The MQTT host defaults to \fClocalhost\fR. 125 | .IP \fCMQTT_PORT\fR 1i 126 | The MQTT port is \fC1883\fR. 127 | .IP \fCMQTT_USER\fR 1i 128 | The username to use to authenticate with the MQTT broker. 129 | .IP \fCMQTT_PASS\fR 1i 130 | The password to use to authenticate with the MQTT broker. 131 | .IP \fCOCLI_CLIENTID\fR 1i 132 | The MQTT clientId is set to \fC"ocli-\fIusername\fR\fC-\fIhostname\fR\fC"\fR. 133 | .IP \fCGPSD_HOST\fR 1i 134 | The address/hostname of the host on which 135 | .B gpsd 136 | is listening defaults to \fClocalhost\fR. 137 | .IP \fCGPSD_PORT\fR 1i 138 | The 139 | .B gpsd 140 | port nummber defaults to \fC2947\fR. 141 | .IP \fCOCLI_TID\fR 1i 142 | The two-letter OwnTracks tracker ID defaults to not being used. 143 | .IP \fCOCLI_INTERVAL\fR 1i 144 | The interval after which 145 | .B ocli 146 | will publish a location position defaults to 1 second and can be changed dynamically (see above). 147 | .IP \fCOCLI_DISPLACEMENT\fR 1i 148 | The distance after which 149 | .B ocli 150 | will publish a location position defaults to 0 meters and can be changed dynamically (see above). 151 | .IP \fCOCLI_CACERT\fR 1i 152 | The path to a PEM-encoded CA certificate which 153 | .B ocli 154 | uses to enable TLS/SSL. It is likely that the default MQTT port will need to be changed at the same time. 155 | .\"----------------------------------------------------------- 156 | .SH BUGS 157 | Very likely. 158 | .\"----------------------------------------------------------- 159 | .SH AUTHOR 160 | Jan-Piet Mens, https://jpmens.net 161 | .\"----------------------------------------------------------- 162 | .SH SEE ALSO 163 | .nh 164 | .BR jo (1), 165 | .BR mosquitto_pub (1), 166 | .BR pledge (2), 167 | .BR unveil (2), 168 | .BR gpsd (8), 169 | .BR https://owntracks.org 170 | .\" EOF ocli.1 171 | -------------------------------------------------------------------------------- /packaging/Makefile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # ========================================== 5 | # configuration 6 | # ========================================== 7 | PACKAGE_PATH = ./dist 8 | PACKAGE_NAME = owntracks-cli-publisher 9 | PACKAGE_VERSION = $(shell cat ../version.h | sed -e 's/\#define VERSION //' | tr -d '"\n') 10 | 11 | 12 | 13 | # ========================================== 14 | # image building 15 | # ========================================== 16 | 17 | # Synopsis: 18 | # 19 | # make build-debian-baseline arch=amd64 dist=buster version=0.1.0 20 | # make build-debian-baseline arch=armv7hf dist=stretch version=0.1.0 21 | # 22 | # make build-centos-baseline arch=amd64 dist=7 version=0.1.0 23 | # make build-centos-baseline arch=arm32v7 dist=7 version=0.1.0 24 | # 25 | 26 | build-debian-baseline: check-image-options 27 | docker build --tag owntracks/baseline-$(dist)-$(arch):$(version) --build-arg BASE_IMAGE=balenalib/$(arch)-debian:$(dist)-build - < dockerfiles/Dockerfile.debian.baseline 28 | docker tag owntracks/baseline-$(dist)-$(arch):$(version) owntracks/baseline-$(dist)-$(arch):latest 29 | 30 | build-centos-baseline: check-image-options 31 | docker build --tag owntracks/baseline-centos$(dist)-$(arch):$(version) --build-arg BASE_IMAGE=$(arch)/centos:$(dist) - < dockerfiles/Dockerfile.centos.baseline 32 | docker tag owntracks/baseline-centos$(dist)-$(arch):$(version) owntracks/baseline-centos$(dist)-$(arch):latest 33 | 34 | check-image-options: 35 | @if test "$(arch)" = ""; then \ 36 | echo "ERROR: 'arch' not set"; \ 37 | exit 1; \ 38 | fi 39 | @if test "$(dist)" = ""; then \ 40 | echo "ERROR: 'dist' not set"; \ 41 | exit 1; \ 42 | fi 43 | @if test "$(version)" = ""; then \ 44 | echo "ERROR: 'version' not set"; \ 45 | exit 1; \ 46 | fi 47 | 48 | 49 | # ========================================== 50 | # packaging 51 | # ========================================== 52 | # 53 | # Build Debian package. 54 | # 55 | # Synopsis:: 56 | # 57 | # # Debian Buster amd64 58 | # make debian-package arch=amd64 dist=buster pkgtype=deb 59 | # 60 | # # Debian Stretch armhf 61 | # make debian-package arch=armhf dist=stretch pkgtype=deb 62 | # 63 | # # CentOS 7 64 | # make debian-package arch=amd64 dist=centos7 pkgtype=rpm 65 | # 66 | 67 | # https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux 68 | # https://en.wikipedia.org/wiki/ANSI_escape_code 69 | YELLOW := \033[0;33m\033[1m 70 | NC := \033[0m 71 | 72 | debian-package: 73 | $(eval name := $(PACKAGE_NAME)) 74 | $(eval package_name := $(name)_$(PACKAGE_VERSION)-1~$(dist)_$(arch).deb) 75 | $(MAKE) pkg-build name=$(name) package_name=$(package_name) 76 | 77 | centos-package: 78 | ifeq ($(arch),armhf) 79 | $(eval name_arch := arm32v7) 80 | else 81 | $(eval name_arch := x86_64) 82 | endif 83 | $(eval name := $(PACKAGE_NAME)) 84 | $(eval package_name := $(name)-$(PACKAGE_VERSION)-1~$(dist).$(name_arch).rpm) 85 | $(MAKE) pkg-build name=$(name) package_name=$(package_name) 86 | 87 | pkg-build: check-build-options 88 | 89 | ifeq ($(arch),armhf) 90 | $(eval build_arch := armv7hf) 91 | else 92 | $(eval build_arch := $(arch)) 93 | endif 94 | 95 | @# Build Linux distribution package 96 | @echo "Building $(YELLOW)$(pkgtype)$(NC) package $(YELLOW)$(name)$(NC) version $(YELLOW)$(PACKAGE_VERSION)$(NC) for architecture $(YELLOW)$(arch)$(NC) and distribution $(YELLOW)$(dist)$(NC)." 97 | 98 | $(eval build_image := owntracks/baseline-$(dist)-$(build_arch):latest) 99 | $(eval build_tag := owntracks/$(PACKAGE_NAME)-build-$(dist)-$(arch):$(PACKAGE_VERSION)) 100 | 101 | @# Invoke the builder. 102 | docker build --tag $(build_tag) --build-arg BASE_IMAGE=${build_image} --build-arg DISTRIBUTION=$(dist) --build-arg PKGTYPE=$(pkgtype) --build-arg VERSION=$(PACKAGE_VERSION) --build-arg NAME=$(name) --file dockerfiles/Dockerfile.builder .. 103 | 104 | 105 | @echo "Harvesting package $(YELLOW)$(package_name)$(NC) from Docker container to directory $(YELLOW)$(PACKAGE_PATH)$(NC)." 106 | 107 | @# Spin up container. 108 | @docker container rm -f finalize; true 109 | docker container create --name finalize $(build_tag) 110 | 111 | @# Copy .deb file from Docker container to working tree. 112 | mkdir -p $(PACKAGE_PATH) 113 | docker container cp finalize:/sources/dist/$(package_name) $(PACKAGE_PATH) 114 | 115 | @# Destroy container. 116 | docker container rm -f finalize 117 | 118 | @echo "Enjoy your package at $(YELLOW)$(PACKAGE_PATH)/$(package_name)$(NC)." 119 | 120 | 121 | check-build-options: 122 | @if test "$(arch)" = ""; then \ 123 | echo "ERROR: 'arch' not set"; \ 124 | exit 1; \ 125 | fi 126 | @if test "$(dist)" = ""; then \ 127 | echo "ERROR: 'dist' not set"; \ 128 | exit 1; \ 129 | fi 130 | @if test "$(pkgtype)" = ""; then \ 131 | echo "ERROR: 'pkgtype' not set"; \ 132 | exit 1; \ 133 | fi 134 | @if test "$(name)" = ""; then \ 135 | echo "ERROR: 'name' not set"; \ 136 | exit 1; \ 137 | fi 138 | 139 | 140 | # ========================================== 141 | # publishing 142 | # ========================================== 143 | # 144 | # Publish Debian package. 145 | # 146 | # Synopsis:: 147 | # 148 | # # Debian amd64 149 | # make publish-package arch=amd64 dist=buster 150 | # 151 | # # Debian armhf 152 | # make debian-package arch=armhf dist=stretch 153 | # 154 | 155 | $(eval github-release := ./bin/github-release) 156 | 157 | publish-package: check-github-release check-publish-options 158 | 159 | $(eval package_file := $(PACKAGE_PATH)/$(PACKAGE_NAME)_$(PACKAGE_VERSION)-1~$(dist)_$(arch).deb) 160 | 161 | @echo "Uploading release artefact $(package_file) to GitHub" 162 | 163 | @# Show current releases. 164 | @#$(github-release) info --user owntracks --repo ocli 165 | 166 | # Create Release. 167 | $(github-release) release --user owntracks --repo ocli --tag $(PACKAGE_VERSION) || true 168 | @# --draft 169 | 170 | # Upload artifact. 171 | $(github-release) upload --user owntracks --repo ocli --tag $(PACKAGE_VERSION) --name $(notdir $(package_file)) --file $(package_file) --replace 172 | 173 | check-publish-options: 174 | @if test "$(arch)" = ""; then \ 175 | echo "ERROR: 'arch' not set"; \ 176 | exit 1; \ 177 | fi 178 | @if test "$(dist)" = ""; then \ 179 | echo "ERROR: 'dist' not set"; \ 180 | exit 1; \ 181 | fi 182 | 183 | 184 | # ---------------------- 185 | # github-release program 186 | # ---------------------- 187 | 188 | check-github-release: 189 | @test -e $(github-release) || (echo 'ERROR: "github-release" not found.\nPlease install "github-release" to "./bin/github-release".\nSee https://github.com/aktau/github-release\n'; exit 1) 190 | 191 | install-github-release: 192 | # https://github.com/aktau/github-release 193 | $(eval url := https://github.com/aktau/github-release/releases/download/v0.7.2/darwin-amd64-github-release.tar.bz2) 194 | 195 | # NotYetImplemented 196 | @exit 1 197 | 198 | @#@test -e $(github-release) || cd tmp; wget $(url) 199 | @#$(eval github-release := $(tools_dir)tmp/bin/darwin/amd64/github-release) 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # owntracks-cli-publisher 2 | 3 | ![OCLI logo](assets/owntrackscli192.png) 4 | 5 | This is the OwnTracks command line interface publisher, a.k.a. _owntracks-cli-publisher_, a small utility which connects to _gpsd_ and publishes position information in [OwnTracks JSON](https://owntracks.org/booklet/tech/json/) to an MQTT broker in order for [compatible software](https://owntracks.org/booklet/guide/clients/) to process location data. (Read up on [what OwnTracks does](https://owntracks.org/booklet/guide/whathow/) if you're new to it.) 6 | 7 | ### gpsd 8 | 9 | [gpsd] is a daemon that receives data from one or more GPS receivers and provides data to multiple applications via a TCP/IP service. GPS data thus obtained is processed by _owntracks-cli-publisher_ and published via [MQTT]. 10 | 11 | ![vk-172](assets/img_9643.jpg) 12 | 13 | We've been successful with a number of GPS receivers, some of which are very affordable. The small USB stick receiver in the above photo is called a VK172 and costs roughly €16. 14 | 15 | Determining which USB port was obtained by the receiver can be challenging, but _dmesg_ is useful to find out. Once you know the port, cross your fingers that it remains identical after a reboot, and ensure your _gpsd_ service is running. For testing, we've found that launching it from a terminal is useful: 16 | 17 | ```console 18 | $ gpsd -n -D 2 -N /dev/tty.usbmodem1A121201 19 | ``` 20 | ### owntracks-cli-publisher 21 | 22 | _owntracks-cli-publisher_ operates with a number of defaults which you can override using environment variables. 23 | 24 | The following defaults are used: 25 | 26 | - The MQTT base topic defaults to `owntracks//`, where _username_ is the name of the logged in user, and _hostname_ the short host name. This base topic can be overridden by setting `BASE_TOPIC` in the environment. 27 | - The MQTT host and port are `localhost` and `1883` respectively, and can be set using `MQTT_HOST` and `MQTT_PORT`. 28 | - The MQTT clientId is set to `"ocli--"`, but it can be overridden by setting `OCLI_CLIENTID` in the environment. 29 | - TCP is used to connect to _gpsd_ with `localhost` and `2947` being the default host and port, overridden by setting `GPSD_HOST` and `GPSD_PORT`. 30 | - The two-letter OwnTracks [tracker ID](https://owntracks.org/booklet/features/tid/) can be configured by setting `OCLI_TID`; it defaults to not being used. 31 | - `OCLI_INTERVAL` defaults to 60 seconds. 32 | - `OCLI_DISPLACEMENT` defaults to 0 meters. 33 | - TLS can be enabled for the MQTT connection by specifying the path to a PEM CA certificate with which to verify peers in `OCLI_CACERT`. Note, that you'll likely need to also specify a different `MQTT_PORT` from the default. 34 | 35 | _owntracks-cli-publisher_ reads GPS data from _gpsd_ and as soon as it has a fix it publishes an OwnTracks payload (see below). _owntracks-cli-publisher_ will subsequently publish a message every `OCLI_INTERVAL` seconds or when it detects it has moved `OCLI_DISPLACEMENT` meters. 36 | 37 | ![owntracks-cli-publisher with OwnTracks on macOS](assets/jmbp-5862.png) 38 | 39 | #### payload 40 | 41 | Any number of path names can be passed as arguments to _owntracks-cli-publisher_ which interprets each in terms of an element which will be added to the OwnTracks JSON. The element name is the base name of the path. If a path points to an executable file the first line of _stdout_ produced by that executable will be used as the _key_'s _value_, otherwise the first line read from the file. In both cases, trailing newlines are removed from values. 42 | 43 | ```console 44 | $ echo 27.2 > parms/temp 45 | $ owntracks-cli-publisher parms/temp contrib/platform 46 | ``` 47 | 48 | In this example, we use a file and a program. When _owntracks-cli-publisher_ produces its JSON we'll see something like this: 49 | 50 | ```json 51 | { 52 | "_type": "location", 53 | "tst": 1577654651, 54 | "lat": 48.856826, 55 | 56 | "temp" : "27.2", 57 | "platform": "FreeBSD" 58 | } 59 | ``` 60 | 61 | Note that a _key_ may not overwrite JSON keys defined by _owntracks-cli-publisher_, so for example, a file called `lat` will not be accepted as it would clobber the latitude JSON element. 62 | 63 | #### controlling owntracks-cli-publisher 64 | 65 | It is possible to control _owntracks-cli-publisher_ using a subset of OwnTrack's `cmd` commands. 66 | 67 | ```console 68 | $ t=owntracks/jpm/tiggr/cmd 69 | $ mosquitto_pub -t $t -m "$(jo _type=cmd action=reportLocation)" 70 | ``` 71 | The following commands are currently implemented: 72 | 73 | - `reportLocation` causes _owntracks-cli-publisher_ to publish its current location (providing _gpsd_ has a fix). _owntracks-cli-publisher_ sets `t:m` in the JSON indicating the publish was manually requested. 74 | - `dump` causes _owntracks-cli-publisher_ to publish its internal configuration to the topic `/dump` as a `_type: configuration` message. 75 | 76 | ```json 77 | { 78 | "_type": "configuration", 79 | "_npubs": 47, 80 | "clientId": "owntracks-ocli", 81 | "locatorInterval": 60, 82 | "locatorDisplacement": 0, 83 | "pubTopicBase": "owntracks/jpm/tiggr", 84 | "tid": "OC", 85 | "username": "jpm", 86 | "deviceId": "tiggr" 87 | } 88 | ``` 89 | 90 | - `setConfiguration` permits setting some of _owntracks-cli-publisher_'s internal values. Note that these do not persist a restart. 91 | 92 | ```console 93 | $ mosquitto_pub -t $t -m "$(jo _type=cmd action=setConfiguration configuration=$(jo _type=configuration locatorInterval=10 locatorDisplacement=0))" 94 | ``` 95 | 96 | ```json 97 | { 98 | "_type": "cmd", 99 | "action": "setConfiguration", 100 | "configuration": { 101 | "_type": "configuration", 102 | "locatorInterval": 10, 103 | "locatorDisplacement": 0 104 | } 105 | } 106 | ``` 107 | 108 | 109 | 110 | ### testing 111 | 112 | There is a small set of scripts with which you can test owntracks-cli-publisher without having a real GPS receiver. Please check [contrib/fake/](contrib/fake/) for more information. 113 | 114 | ### building 115 | 116 | _owntracks-cli-publisher_ should compile easily once you extract the source code and have the prerequisite libraries installed for linking against _gpsd_ and the _mosquitto_ library. 117 | 118 | Systems we've tested on require the following packages in order to build _owntracks-cli-publisher_. 119 | 120 | #### FreeBSD 121 | 122 | ```console 123 | # pkg install mosquitto gpsd 124 | $ make 125 | ``` 126 | 127 | #### OpenBSD 128 | 129 | ```console 130 | # pkg_add mosquitto gpsd 131 | $ make 132 | ``` 133 | 134 | #### macOS 135 | 136 | ```console 137 | $ brew install mosquitto gpsd 138 | $ make 139 | ``` 140 | 141 | #### Debian 142 | 143 | ```console 144 | # apt-get install libmosquitto-dev libgps-dev 145 | $ make 146 | ``` 147 | 148 | #### systemd 149 | 150 | This may be a way of getting _owntracks-cli-publisher_ working on machines with _systemd_. Basically we need two things: 151 | 152 | 1. an environment file, `owntracks-cli-publisher.env`: 153 | 154 | ``` 155 | name="Testing" 156 | fixlog="/tmp/fix.log" 157 | BASE_TOPIC="m/bus/b001" 158 | MQTT_HOST="localhost" 159 | MQTT_PORT=1888 160 | GPSD_HOST="localhost" 161 | GPSD_PORT="2947" 162 | OCLI_TID="OC" 163 | ``` 164 | 165 | 2. a systemd Unit file: 166 | 167 | ``` 168 | [Unit] 169 | Description=OwnTracks cli 170 | Requires=mosquitto.service 171 | 172 | [Service] 173 | Type=simple 174 | EnvironmentFile=/home/jpm/owntracks-cli-publisher.env 175 | ExecStartPre=/usr/bin/touch /tmp/ocli-started-${name} 176 | ExecStart=/home/jpm/bin/owntracks-cli-publisher 177 | Restart=always 178 | RestartSec=60 179 | User=mosquitto 180 | Group=mosquitto 181 | 182 | [Install] 183 | WantedBy=multi-user.target 184 | ``` 185 | 186 | ### Packages 187 | 188 | Packages are available for select operating systems from the Github releases page. Note that the RPM packages do not contain an `owntracks-cli-publisher.env` file for systemd. 189 | 190 | 191 | ### Credits 192 | 193 | - Idea and initial implementation by [Jan-Piet Mens](https://jpmens.net) 194 | - [gpsd] 195 | - [mosquitto](https://mosquitto.org) 196 | - [utarray](https://troydhanson.github.io/uthash/utarray.html) 197 | - [utstring](https://troydhanson.github.io/uthash/utstring.html) 198 | - packaging work by [Andreas Motl](https://github.com/amotl) 199 | 200 | [gpsd]: https://gpsd.gitlab.io/gpsd/ 201 | [mqtt]: http://mqtt.org 202 | -------------------------------------------------------------------------------- /contrib/fake/paris.nmea: -------------------------------------------------------------------------------- 1 | $GPGGA,134641.931,4851.422,N,00217.215,E,1,12,1.0,0.0,M,0.0,M,,*6E 2 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 3 | $GPRMC,134641.931,A,4851.422,N,00217.215,E,019.4,052.7,050120,000.0,W*70 4 | $GPGGA,134651.931,4851.463,N,00217.269,E,1,12,1.0,0.0,M,0.0,M,,*61 5 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 6 | $GPRMC,134651.931,A,4851.463,N,00217.269,E,019.4,052.7,050120,000.0,W*7F 7 | $GPGGA,134701.931,4851.504,N,00217.322,E,1,12,1.0,0.0,M,0.0,M,,*6B 8 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 9 | $GPRMC,134701.931,A,4851.504,N,00217.322,E,019.4,052.7,050120,000.0,W*75 10 | $GPGGA,134711.931,4851.544,N,00217.376,E,1,12,1.0,0.0,M,0.0,M,,*6F 11 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 12 | $GPRMC,134711.931,A,4851.544,N,00217.376,E,019.4,052.7,050120,000.0,W*71 13 | $GPGGA,134721.931,4851.585,N,00217.430,E,1,12,1.0,0.0,M,0.0,M,,*64 14 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 15 | $GPRMC,134721.931,A,4851.585,N,00217.430,E,019.4,052.7,050120,000.0,W*7A 16 | $GPGGA,134731.931,4851.626,N,00217.483,E,1,12,1.0,0.0,M,0.0,M,,*67 17 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 18 | $GPRMC,134731.931,A,4851.626,N,00217.483,E,019.4,052.7,050120,000.0,W*79 19 | $GPGGA,134741.931,4851.667,N,00217.537,E,1,12,1.0,0.0,M,0.0,M,,*6B 20 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 21 | $GPRMC,134741.931,A,4851.667,N,00217.537,E,019.4,052.7,050120,000.0,W*75 22 | $GPGGA,134751.931,4851.708,N,00217.591,E,1,12,1.0,0.0,M,0.0,M,,*6E 23 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 24 | $GPRMC,134751.931,A,4851.708,N,00217.591,E,019.4,073.4,050120,000.0,W*70 25 | $GPGGA,134801.931,4851.730,N,00217.666,E,1,12,1.0,0.0,M,0.0,M,,*64 26 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 27 | $GPRMC,134801.931,A,4851.730,N,00217.666,E,019.4,073.4,050120,000.0,W*7A 28 | $GPGGA,134811.931,4851.752,N,00217.740,E,1,12,1.0,0.0,M,0.0,M,,*64 29 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 30 | $GPRMC,134811.931,A,4851.752,N,00217.740,E,019.4,073.4,050120,000.0,W*7A 31 | $GPGGA,134821.931,4851.774,N,00217.815,E,1,12,1.0,0.0,M,0.0,M,,*6C 32 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 33 | $GPRMC,134821.931,A,4851.774,N,00217.815,E,019.4,075.6,050120,000.0,W*76 34 | $GPGGA,134831.931,4851.794,N,00217.892,E,1,12,1.0,0.0,M,0.0,M,,*6C 35 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 36 | $GPRMC,134831.931,A,4851.794,N,00217.892,E,019.4,075.6,050120,000.0,W*76 37 | $GPGGA,134841.931,4851.813,N,00217.968,E,1,12,1.0,0.0,M,0.0,M,,*6F 38 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 39 | $GPRMC,134841.931,A,4851.813,N,00217.968,E,019.4,075.6,050120,000.0,W*75 40 | $GPGGA,134851.931,4851.833,N,00218.045,E,1,12,1.0,0.0,M,0.0,M,,*65 41 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 42 | $GPRMC,134851.931,A,4851.833,N,00218.045,E,019.4,086.1,050120,000.0,W*74 43 | $GPGGA,134901.931,4851.838,N,00218.126,E,1,12,1.0,0.0,M,0.0,M,,*6E 44 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 45 | $GPRMC,134901.931,A,4851.838,N,00218.126,E,019.4,086.1,050120,000.0,W*7F 46 | $GPGGA,134911.931,4851.844,N,00218.208,E,1,12,1.0,0.0,M,0.0,M,,*6B 47 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 48 | $GPRMC,134911.931,A,4851.844,N,00218.208,E,019.4,086.1,050120,000.0,W*7A 49 | $GPGGA,134921.931,4851.849,N,00218.289,E,1,12,1.0,0.0,M,0.0,M,,*6C 50 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 51 | $GPRMC,134921.931,A,4851.849,N,00218.289,E,019.4,086.1,050120,000.0,W*7D 52 | $GPGGA,134931.931,4851.855,N,00218.371,E,1,12,1.0,0.0,M,0.0,M,,*66 53 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 54 | $GPRMC,134931.931,A,4851.855,N,00218.371,E,019.4,086.4,050120,000.0,W*72 55 | $GPGGA,134941.931,4851.860,N,00218.453,E,1,12,1.0,0.0,M,0.0,M,,*60 56 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 57 | $GPRMC,134941.931,A,4851.860,N,00218.453,E,019.4,086.4,050120,000.0,W*74 58 | $GPGGA,134951.931,4851.865,N,00218.534,E,1,12,1.0,0.0,M,0.0,M,,*64 59 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 60 | $GPRMC,134951.931,A,4851.865,N,00218.534,E,019.4,086.4,050120,000.0,W*70 61 | $GPGGA,135001.931,4851.870,N,00218.616,E,1,12,1.0,0.0,M,0.0,M,,*6E 62 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 63 | $GPRMC,135001.931,A,4851.870,N,00218.616,E,019.4,087.4,050120,000.0,W*7B 64 | $GPGGA,135011.931,4851.874,N,00218.698,E,1,12,1.0,0.0,M,0.0,M,,*6D 65 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 66 | $GPRMC,135011.931,A,4851.874,N,00218.698,E,019.4,087.4,050120,000.0,W*78 67 | $GPGGA,135021.931,4851.877,N,00218.780,E,1,12,1.0,0.0,M,0.0,M,,*65 68 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 69 | $GPRMC,135021.931,A,4851.877,N,00218.780,E,019.4,165.6,050120,000.0,W*7F 70 | $GPGGA,135031.931,4851.824,N,00218.793,E,1,12,1.0,0.0,M,0.0,M,,*60 71 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 72 | $GPRMC,135031.931,A,4851.824,N,00218.793,E,019.4,168.6,050120,000.0,W*77 73 | $GPGGA,135041.931,4851.771,N,00218.804,E,1,12,1.0,0.0,M,0.0,M,,*69 74 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 75 | $GPRMC,135041.931,A,4851.771,N,00218.804,E,019.4,262.8,050120,000.0,W*79 76 | $GPGGA,135051.931,4851.761,N,00218.724,E,1,12,1.0,0.0,M,0.0,M,,*64 77 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 78 | $GPRMC,135051.931,A,4851.761,N,00218.724,E,019.4,262.8,050120,000.0,W*74 79 | $GPGGA,135101.931,4851.751,N,00218.643,E,1,12,1.0,0.0,M,0.0,M,,*63 80 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 81 | $GPRMC,135101.931,A,4851.751,N,00218.643,E,019.4,268.0,050120,000.0,W*71 82 | $GPGGA,135111.931,4851.748,N,00218.561,E,1,12,1.0,0.0,M,0.0,M,,*69 83 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 84 | $GPRMC,135111.931,A,4851.748,N,00218.561,E,019.4,268.0,050120,000.0,W*7B 85 | $GPGGA,135121.931,4851.745,N,00218.479,E,1,12,1.0,0.0,M,0.0,M,,*6F 86 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 87 | $GPRMC,135121.931,A,4851.745,N,00218.479,E,019.4,268.0,050120,000.0,W*7D 88 | $GPGGA,135131.931,4851.742,N,00218.397,E,1,12,1.0,0.0,M,0.0,M,,*6E 89 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 90 | $GPRMC,135131.931,A,4851.742,N,00218.397,E,019.4,268.0,050120,000.0,W*7C 91 | $GPGGA,135141.931,4851.739,N,00218.315,E,1,12,1.0,0.0,M,0.0,M,,*6F 92 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 93 | $GPRMC,135141.931,A,4851.739,N,00218.315,E,019.4,268.0,050120,000.0,W*7D 94 | $GPGGA,135151.931,4851.737,N,00218.234,E,1,12,1.0,0.0,M,0.0,M,,*62 95 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 96 | $GPRMC,135151.931,A,4851.737,N,00218.234,E,019.4,268.0,050120,000.0,W*70 97 | $GPGGA,135201.931,4851.734,N,00218.152,E,1,12,1.0,0.0,M,0.0,M,,*64 98 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 99 | $GPRMC,135201.931,A,4851.734,N,00218.152,E,019.4,207.6,050120,000.0,W*79 100 | $GPGGA,135211.931,4851.683,N,00218.125,E,1,12,1.0,0.0,M,0.0,M,,*68 101 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 102 | $GPRMC,135211.931,A,4851.683,N,00218.125,E,019.4,207.6,050120,000.0,W*75 103 | $GPGGA,135221.931,4851.632,N,00218.098,E,1,12,1.0,0.0,M,0.0,M,,*66 104 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 105 | $GPRMC,135221.931,A,4851.632,N,00218.098,E,019.4,207.6,050120,000.0,W*7B 106 | $GPGGA,135231.931,4851.581,N,00218.071,E,1,12,1.0,0.0,M,0.0,M,,*6B 107 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 108 | $GPRMC,135231.931,A,4851.581,N,00218.071,E,019.4,207.6,050120,000.0,W*76 109 | $GPGGA,135241.931,4851.530,N,00218.045,E,1,12,1.0,0.0,M,0.0,M,,*61 110 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 111 | $GPRMC,135241.931,A,4851.530,N,00218.045,E,019.4,207.6,050120,000.0,W*7C 112 | $GPGGA,135251.931,4851.479,N,00218.018,E,1,12,1.0,0.0,M,0.0,M,,*64 113 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 114 | $GPRMC,135251.931,A,4851.479,N,00218.018,E,019.4,237.6,050120,000.0,W*7A 115 | $GPGGA,135301.931,4851.441,N,00217.959,E,1,12,1.0,0.0,M,0.0,M,,*68 116 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 117 | $GPRMC,135301.931,A,4851.441,N,00217.959,E,019.4,237.6,050120,000.0,W*76 118 | $GPGGA,135311.931,4851.404,N,00217.900,E,1,12,1.0,0.0,M,0.0,M,,*64 119 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 120 | $GPRMC,135311.931,A,4851.404,N,00217.900,E,019.4,232.9,050120,000.0,W*70 121 | $GPGGA,135321.931,4851.363,N,00217.846,E,1,12,1.0,0.0,M,0.0,M,,*62 122 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 123 | $GPRMC,135321.931,A,4851.363,N,00217.846,E,019.4,232.9,050120,000.0,W*76 124 | $GPGGA,135331.931,4851.323,N,00217.792,E,1,12,1.0,0.0,M,0.0,M,,*61 125 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 126 | $GPRMC,135331.931,A,4851.323,N,00217.792,E,019.4,232.9,050120,000.0,W*75 127 | $GPGGA,135341.931,4851.282,N,00217.738,E,1,12,1.0,0.0,M,0.0,M,,*6C 128 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 129 | $GPRMC,135341.931,A,4851.282,N,00217.738,E,019.4,234.8,050120,000.0,W*7F 130 | $GPGGA,135351.931,4851.243,N,00217.682,E,1,12,1.0,0.0,M,0.0,M,,*60 131 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 132 | $GPRMC,135351.931,A,4851.243,N,00217.682,E,019.4,234.8,050120,000.0,W*73 133 | $GPGGA,135401.931,4851.203,N,00217.626,E,1,12,1.0,0.0,M,0.0,M,,*68 134 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 135 | $GPRMC,135401.931,A,4851.203,N,00217.626,E,019.4,234.8,050120,000.0,W*7B 136 | $GPGGA,135411.931,4851.164,N,00217.570,E,1,12,1.0,0.0,M,0.0,M,,*6B 137 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 138 | $GPRMC,135411.931,A,4851.164,N,00217.570,E,019.4,234.8,050120,000.0,W*78 139 | $GPGGA,135421.931,4851.125,N,00217.514,E,1,12,1.0,0.0,M,0.0,M,,*6F 140 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 141 | $GPRMC,135421.931,A,4851.125,N,00217.514,E,019.4,312.4,050120,000.0,W*75 142 | $GPGGA,135431.931,4851.168,N,00217.466,E,1,12,1.0,0.0,M,0.0,M,,*63 143 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 144 | $GPRMC,135431.931,A,4851.168,N,00217.466,E,019.4,312.4,050120,000.0,W*79 145 | $GPGGA,135441.931,4851.212,N,00217.418,E,1,12,1.0,0.0,M,0.0,M,,*63 146 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 147 | $GPRMC,135441.931,A,4851.212,N,00217.418,E,019.4,312.4,050120,000.0,W*79 148 | $GPGGA,135451.931,4851.256,N,00217.370,E,1,12,1.0,0.0,M,0.0,M,,*6B 149 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 150 | $GPRMC,135451.931,A,4851.256,N,00217.370,E,019.4,312.4,050120,000.0,W*71 151 | $GPGGA,135501.931,4851.299,N,00217.322,E,1,12,1.0,0.0,M,0.0,M,,*6B 152 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 153 | $GPRMC,135501.931,A,4851.299,N,00217.322,E,019.4,312.4,050120,000.0,W*71 154 | $GPGGA,135511.931,4851.343,N,00217.274,E,1,12,1.0,0.0,M,0.0,M,,*6E 155 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 156 | $GPRMC,135511.931,A,4851.343,N,00217.274,E,019.4,312.4,050120,000.0,W*74 157 | $GPGGA,135521.931,4851.387,N,00217.226,E,1,12,1.0,0.0,M,0.0,M,,*62 158 | $GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30 159 | $GPRMC,135521.931,A,4851.387,N,00217.226,E,019.4,312.4,050120,000.0,W*78 160 | -------------------------------------------------------------------------------- /utstring.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2014, Troy D. Hanson http://troydhanson.github.com/uthash/ 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 12 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 13 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 14 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 15 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 16 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 18 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 19 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | */ 23 | 24 | /* a dynamic string implementation using macros 25 | */ 26 | #ifndef UTSTRING_H 27 | #define UTSTRING_H 28 | 29 | #define UTSTRING_VERSION 1.9.9 30 | 31 | #ifdef __GNUC__ 32 | #define _UNUSED_ __attribute__ ((__unused__)) 33 | #else 34 | #define _UNUSED_ 35 | #endif 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #define oom() exit(-1) 42 | 43 | typedef struct { 44 | char *d; 45 | size_t n; /* allocd size */ 46 | size_t i; /* index of first unused byte */ 47 | } UT_string; 48 | 49 | #define utstring_reserve(s,amt) \ 50 | do { \ 51 | if (((s)->n - (s)->i) < (size_t)(amt)) { \ 52 | (s)->d = (char*)realloc((s)->d, (s)->n + (amt)); \ 53 | if ((s)->d == NULL) oom(); \ 54 | (s)->n += (amt); \ 55 | } \ 56 | } while(0) 57 | 58 | #define utstring_init(s) \ 59 | do { \ 60 | (s)->n = 0; (s)->i = 0; (s)->d = NULL; \ 61 | utstring_reserve(s,100); \ 62 | (s)->d[0] = '\0'; \ 63 | } while(0) 64 | 65 | #define utstring_done(s) \ 66 | do { \ 67 | if ((s)->d != NULL) free((s)->d); \ 68 | (s)->n = 0; \ 69 | } while(0) 70 | 71 | #define utstring_free(s) \ 72 | do { \ 73 | utstring_done(s); \ 74 | free(s); \ 75 | } while(0) 76 | 77 | #define utstring_new(s) \ 78 | do { \ 79 | s = (UT_string*)calloc(sizeof(UT_string),1); \ 80 | if (!s) oom(); \ 81 | utstring_init(s); \ 82 | } while(0) 83 | 84 | #define utstring_renew(s) \ 85 | do { \ 86 | if (s) { \ 87 | utstring_clear(s); \ 88 | } else { \ 89 | utstring_new(s); \ 90 | } \ 91 | } while(0) 92 | 93 | #define utstring_clear(s) \ 94 | do { \ 95 | (s)->i = 0; \ 96 | (s)->d[0] = '\0'; \ 97 | } while(0) 98 | 99 | #define utstring_bincpy(s,b,l) \ 100 | do { \ 101 | utstring_reserve((s),(l)+1); \ 102 | if (l) memcpy(&(s)->d[(s)->i], b, l); \ 103 | (s)->i += (l); \ 104 | (s)->d[(s)->i]='\0'; \ 105 | } while(0) 106 | 107 | #define utstring_concat(dst,src) \ 108 | do { \ 109 | utstring_reserve((dst),((src)->i)+1); \ 110 | if ((src)->i) memcpy(&(dst)->d[(dst)->i], (src)->d, (src)->i); \ 111 | (dst)->i += (src)->i; \ 112 | (dst)->d[(dst)->i]='\0'; \ 113 | } while(0) 114 | 115 | #define utstring_len(s) ((unsigned)((s)->i)) 116 | 117 | #define utstring_body(s) ((s)->d) 118 | 119 | _UNUSED_ static void utstring_printf_va(UT_string *s, const char *fmt, va_list ap) { 120 | int n; 121 | va_list cp; 122 | while (1) { 123 | #ifdef _WIN32 124 | cp = ap; 125 | #else 126 | va_copy(cp, ap); 127 | #endif 128 | n = vsnprintf (&s->d[s->i], s->n-s->i, fmt, cp); 129 | va_end(cp); 130 | 131 | if ((n > -1) && ((size_t) n < (s->n-s->i))) { 132 | s->i += n; 133 | return; 134 | } 135 | 136 | /* Else try again with more space. */ 137 | if (n > -1) utstring_reserve(s,n+1); /* exact */ 138 | else utstring_reserve(s,(s->n)*2); /* 2x */ 139 | } 140 | } 141 | #ifdef __GNUC__ 142 | /* support printf format checking (2=the format string, 3=start of varargs) */ 143 | static void utstring_printf(UT_string *s, const char *fmt, ...) 144 | __attribute__ (( format( printf, 2, 3) )); 145 | #endif 146 | _UNUSED_ static void utstring_printf(UT_string *s, const char *fmt, ...) { 147 | va_list ap; 148 | va_start(ap,fmt); 149 | utstring_printf_va(s,fmt,ap); 150 | va_end(ap); 151 | } 152 | 153 | /******************************************************************************* 154 | * begin substring search functions * 155 | ******************************************************************************/ 156 | /* Build KMP table from left to right. */ 157 | _UNUSED_ static void _utstring_BuildTable( 158 | const char *P_Needle, 159 | size_t P_NeedleLen, 160 | long *P_KMP_Table) 161 | { 162 | long i, j; 163 | 164 | i = 0; 165 | j = i - 1; 166 | P_KMP_Table[i] = j; 167 | while (i < (long) P_NeedleLen) 168 | { 169 | while ( (j > -1) && (P_Needle[i] != P_Needle[j]) ) 170 | { 171 | j = P_KMP_Table[j]; 172 | } 173 | i++; 174 | j++; 175 | if (i < (long) P_NeedleLen) 176 | { 177 | if (P_Needle[i] == P_Needle[j]) 178 | { 179 | P_KMP_Table[i] = P_KMP_Table[j]; 180 | } 181 | else 182 | { 183 | P_KMP_Table[i] = j; 184 | } 185 | } 186 | else 187 | { 188 | P_KMP_Table[i] = j; 189 | } 190 | } 191 | 192 | return; 193 | } 194 | 195 | 196 | /* Build KMP table from right to left. */ 197 | _UNUSED_ static void _utstring_BuildTableR( 198 | const char *P_Needle, 199 | size_t P_NeedleLen, 200 | long *P_KMP_Table) 201 | { 202 | long i, j; 203 | 204 | i = P_NeedleLen - 1; 205 | j = i + 1; 206 | P_KMP_Table[i + 1] = j; 207 | while (i >= 0) 208 | { 209 | while ( (j < (long) P_NeedleLen) && (P_Needle[i] != P_Needle[j]) ) 210 | { 211 | j = P_KMP_Table[j + 1]; 212 | } 213 | i--; 214 | j--; 215 | if (i >= 0) 216 | { 217 | if (P_Needle[i] == P_Needle[j]) 218 | { 219 | P_KMP_Table[i + 1] = P_KMP_Table[j + 1]; 220 | } 221 | else 222 | { 223 | P_KMP_Table[i + 1] = j; 224 | } 225 | } 226 | else 227 | { 228 | P_KMP_Table[i + 1] = j; 229 | } 230 | } 231 | 232 | return; 233 | } 234 | 235 | 236 | /* Search data from left to right. ( Multiple search mode. ) */ 237 | _UNUSED_ static long _utstring_find( 238 | const char *P_Haystack, 239 | size_t P_HaystackLen, 240 | const char *P_Needle, 241 | size_t P_NeedleLen, 242 | long *P_KMP_Table) 243 | { 244 | long i, j; 245 | long V_FindPosition = -1; 246 | 247 | /* Search from left to right. */ 248 | i = j = 0; 249 | while ( (j < (int)P_HaystackLen) && (((P_HaystackLen - j) + i) >= P_NeedleLen) ) 250 | { 251 | while ( (i > -1) && (P_Needle[i] != P_Haystack[j]) ) 252 | { 253 | i = P_KMP_Table[i]; 254 | } 255 | i++; 256 | j++; 257 | if (i >= (int)P_NeedleLen) 258 | { 259 | /* Found. */ 260 | V_FindPosition = j - i; 261 | break; 262 | } 263 | } 264 | 265 | return V_FindPosition; 266 | } 267 | 268 | 269 | /* Search data from right to left. ( Multiple search mode. ) */ 270 | _UNUSED_ static long _utstring_findR( 271 | const char *P_Haystack, 272 | size_t P_HaystackLen, 273 | const char *P_Needle, 274 | size_t P_NeedleLen, 275 | long *P_KMP_Table) 276 | { 277 | long i, j; 278 | long V_FindPosition = -1; 279 | 280 | /* Search from right to left. */ 281 | j = (P_HaystackLen - 1); 282 | i = (P_NeedleLen - 1); 283 | while ( (j >= 0) && (j >= i) ) 284 | { 285 | while ( (i < (int)P_NeedleLen) && (P_Needle[i] != P_Haystack[j]) ) 286 | { 287 | i = P_KMP_Table[i + 1]; 288 | } 289 | i--; 290 | j--; 291 | if (i < 0) 292 | { 293 | /* Found. */ 294 | V_FindPosition = j + 1; 295 | break; 296 | } 297 | } 298 | 299 | return V_FindPosition; 300 | } 301 | 302 | 303 | /* Search data from left to right. ( One time search mode. ) */ 304 | _UNUSED_ static long utstring_find( 305 | UT_string *s, 306 | long P_StartPosition, /* Start from 0. -1 means last position. */ 307 | const char *P_Needle, 308 | size_t P_NeedleLen) 309 | { 310 | long V_StartPosition; 311 | long V_HaystackLen; 312 | long *V_KMP_Table; 313 | long V_FindPosition = -1; 314 | 315 | if (P_StartPosition < 0) 316 | { 317 | V_StartPosition = s->i + P_StartPosition; 318 | } 319 | else 320 | { 321 | V_StartPosition = P_StartPosition; 322 | } 323 | V_HaystackLen = s->i - V_StartPosition; 324 | if ( (V_HaystackLen >= (long) P_NeedleLen) && (P_NeedleLen > 0) ) 325 | { 326 | V_KMP_Table = (long *)malloc(sizeof(long) * (P_NeedleLen + 1)); 327 | if (V_KMP_Table != NULL) 328 | { 329 | _utstring_BuildTable(P_Needle, P_NeedleLen, V_KMP_Table); 330 | 331 | V_FindPosition = _utstring_find(s->d + V_StartPosition, 332 | V_HaystackLen, 333 | P_Needle, 334 | P_NeedleLen, 335 | V_KMP_Table); 336 | if (V_FindPosition >= 0) 337 | { 338 | V_FindPosition += V_StartPosition; 339 | } 340 | 341 | free(V_KMP_Table); 342 | } 343 | } 344 | 345 | return V_FindPosition; 346 | } 347 | 348 | 349 | /* Search data from right to left. ( One time search mode. ) */ 350 | _UNUSED_ static long utstring_findR( 351 | UT_string *s, 352 | long P_StartPosition, /* Start from 0. -1 means last position. */ 353 | const char *P_Needle, 354 | size_t P_NeedleLen) 355 | { 356 | long V_StartPosition; 357 | long V_HaystackLen; 358 | long *V_KMP_Table; 359 | long V_FindPosition = -1; 360 | 361 | if (P_StartPosition < 0) 362 | { 363 | V_StartPosition = s->i + P_StartPosition; 364 | } 365 | else 366 | { 367 | V_StartPosition = P_StartPosition; 368 | } 369 | V_HaystackLen = V_StartPosition + 1; 370 | if ( (V_HaystackLen >= (long) P_NeedleLen) && (P_NeedleLen > 0) ) 371 | { 372 | V_KMP_Table = (long *)malloc(sizeof(long) * (P_NeedleLen + 1)); 373 | if (V_KMP_Table != NULL) 374 | { 375 | _utstring_BuildTableR(P_Needle, P_NeedleLen, V_KMP_Table); 376 | 377 | V_FindPosition = _utstring_findR(s->d, 378 | V_HaystackLen, 379 | P_Needle, 380 | P_NeedleLen, 381 | V_KMP_Table); 382 | 383 | free(V_KMP_Table); 384 | } 385 | } 386 | 387 | return V_FindPosition; 388 | } 389 | /******************************************************************************* 390 | * end substring search functions * 391 | ******************************************************************************/ 392 | 393 | #endif /* UTSTRING_H */ 394 | -------------------------------------------------------------------------------- /utarray.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2014, Troy D. Hanson http://troydhanson.github.com/uthash/ 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 12 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 13 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 14 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 15 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 16 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 18 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 19 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | */ 23 | 24 | /* a dynamic array implementation using macros 25 | */ 26 | #ifndef UTARRAY_H 27 | #define UTARRAY_H 28 | 29 | #define UTARRAY_VERSION 1.9.9 30 | 31 | #ifdef __GNUC__ 32 | #define _UNUSED_ __attribute__ ((__unused__)) 33 | #else 34 | #define _UNUSED_ 35 | #endif 36 | 37 | #include /* size_t */ 38 | #include /* memset, etc */ 39 | #include /* exit */ 40 | 41 | #define oom() exit(-1) 42 | 43 | typedef void (ctor_f)(void *dst, const void *src); 44 | typedef void (dtor_f)(void *elt); 45 | typedef void (init_f)(void *elt); 46 | typedef struct { 47 | size_t sz; 48 | init_f *init; 49 | ctor_f *copy; 50 | dtor_f *dtor; 51 | } UT_icd; 52 | 53 | typedef struct { 54 | unsigned i,n;/* i: index of next available slot, n: num slots */ 55 | UT_icd icd; /* initializer, copy and destructor functions */ 56 | char *d; /* n slots of size icd->sz*/ 57 | } UT_array; 58 | 59 | #define utarray_init(a,_icd) do { \ 60 | memset(a,0,sizeof(UT_array)); \ 61 | (a)->icd=*_icd; \ 62 | } while(0) 63 | 64 | #define utarray_done(a) do { \ 65 | if ((a)->n) { \ 66 | if ((a)->icd.dtor) { \ 67 | size_t _ut_i; \ 68 | for(_ut_i=0; _ut_i < (a)->i; _ut_i++) { \ 69 | (a)->icd.dtor(utarray_eltptr(a,_ut_i)); \ 70 | } \ 71 | } \ 72 | free((a)->d); \ 73 | } \ 74 | (a)->n=0; \ 75 | } while(0) 76 | 77 | #define utarray_new(a,_icd) do { \ 78 | a=(UT_array*)malloc(sizeof(UT_array)); \ 79 | utarray_init(a,_icd); \ 80 | } while(0) 81 | 82 | #define utarray_free(a) do { \ 83 | utarray_done(a); \ 84 | free(a); \ 85 | } while(0) 86 | 87 | #define utarray_reserve(a,by) do { \ 88 | if (((a)->i+(by)) > ((a)->n)) { \ 89 | while(((a)->i+(by)) > ((a)->n)) { (a)->n = ((a)->n ? (2*(a)->n) : 8); } \ 90 | if ( ((a)->d=(char*)realloc((a)->d, (a)->n*(a)->icd.sz)) == NULL) oom(); \ 91 | } \ 92 | } while(0) 93 | 94 | #define utarray_push_back(a,p) do { \ 95 | utarray_reserve(a,1); \ 96 | if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,(a)->i++), p); } \ 97 | else { memcpy(_utarray_eltptr(a,(a)->i++), p, (a)->icd.sz); }; \ 98 | } while(0) 99 | 100 | #define utarray_pop_back(a) do { \ 101 | if ((a)->icd.dtor) { (a)->icd.dtor( _utarray_eltptr(a,--((a)->i))); } \ 102 | else { (a)->i--; } \ 103 | } while(0) 104 | 105 | #define utarray_extend_back(a) do { \ 106 | utarray_reserve(a,1); \ 107 | if ((a)->icd.init) { (a)->icd.init(_utarray_eltptr(a,(a)->i)); } \ 108 | else { memset(_utarray_eltptr(a,(a)->i),0,(a)->icd.sz); } \ 109 | (a)->i++; \ 110 | } while(0) 111 | 112 | #define utarray_len(a) ((a)->i) 113 | 114 | #define utarray_eltptr(a,j) (((j) < (a)->i) ? _utarray_eltptr(a,j) : NULL) 115 | #define _utarray_eltptr(a,j) ((char*)((a)->d + ((a)->icd.sz*(j) ))) 116 | 117 | #define utarray_insert(a,p,j) do { \ 118 | if (j > (a)->i) utarray_resize(a,j); \ 119 | utarray_reserve(a,1); \ 120 | if ((j) < (a)->i) { \ 121 | memmove( _utarray_eltptr(a,(j)+1), _utarray_eltptr(a,j), \ 122 | ((a)->i - (j))*((a)->icd.sz)); \ 123 | } \ 124 | if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,j), p); } \ 125 | else { memcpy(_utarray_eltptr(a,j), p, (a)->icd.sz); }; \ 126 | (a)->i++; \ 127 | } while(0) 128 | 129 | #define utarray_inserta(a,w,j) do { \ 130 | if (utarray_len(w) == 0) break; \ 131 | if (j > (a)->i) utarray_resize(a,j); \ 132 | utarray_reserve(a,utarray_len(w)); \ 133 | if ((j) < (a)->i) { \ 134 | memmove(_utarray_eltptr(a,(j)+utarray_len(w)), \ 135 | _utarray_eltptr(a,j), \ 136 | ((a)->i - (j))*((a)->icd.sz)); \ 137 | } \ 138 | if ((a)->icd.copy) { \ 139 | size_t _ut_i; \ 140 | for(_ut_i=0;_ut_i<(w)->i;_ut_i++) { \ 141 | (a)->icd.copy(_utarray_eltptr(a,j+_ut_i), _utarray_eltptr(w,_ut_i)); \ 142 | } \ 143 | } else { \ 144 | memcpy(_utarray_eltptr(a,j), _utarray_eltptr(w,0), \ 145 | utarray_len(w)*((a)->icd.sz)); \ 146 | } \ 147 | (a)->i += utarray_len(w); \ 148 | } while(0) 149 | 150 | #define utarray_resize(dst,num) do { \ 151 | size_t _ut_i; \ 152 | if (dst->i > (size_t)(num)) { \ 153 | if ((dst)->icd.dtor) { \ 154 | for(_ut_i=num; _ut_i < dst->i; _ut_i++) { \ 155 | (dst)->icd.dtor(utarray_eltptr(dst,_ut_i)); \ 156 | } \ 157 | } \ 158 | } else if (dst->i < (size_t)(num)) { \ 159 | utarray_reserve(dst,num-dst->i); \ 160 | if ((dst)->icd.init) { \ 161 | for(_ut_i=dst->i; _ut_i < num; _ut_i++) { \ 162 | (dst)->icd.init(utarray_eltptr(dst,_ut_i)); \ 163 | } \ 164 | } else { \ 165 | memset(_utarray_eltptr(dst,dst->i),0,(dst)->icd.sz*(num-dst->i)); \ 166 | } \ 167 | } \ 168 | dst->i = num; \ 169 | } while(0) 170 | 171 | #define utarray_concat(dst,src) do { \ 172 | utarray_inserta((dst),(src),utarray_len(dst)); \ 173 | } while(0) 174 | 175 | #define utarray_erase(a,pos,len) do { \ 176 | if ((a)->icd.dtor) { \ 177 | size_t _ut_i; \ 178 | for(_ut_i=0; _ut_i < len; _ut_i++) { \ 179 | (a)->icd.dtor(utarray_eltptr((a),pos+_ut_i)); \ 180 | } \ 181 | } \ 182 | if ((a)->i > (pos+len)) { \ 183 | memmove( _utarray_eltptr((a),pos), _utarray_eltptr((a),pos+len), \ 184 | (((a)->i)-(pos+len))*((a)->icd.sz)); \ 185 | } \ 186 | (a)->i -= (len); \ 187 | } while(0) 188 | 189 | #define utarray_renew(a,u) do { \ 190 | if (a) utarray_clear(a); \ 191 | else utarray_new((a),(u)); \ 192 | } while(0) 193 | 194 | #define utarray_clear(a) do { \ 195 | if ((a)->i > 0) { \ 196 | if ((a)->icd.dtor) { \ 197 | size_t _ut_i; \ 198 | for(_ut_i=0; _ut_i < (a)->i; _ut_i++) { \ 199 | (a)->icd.dtor(utarray_eltptr(a,_ut_i)); \ 200 | } \ 201 | } \ 202 | (a)->i = 0; \ 203 | } \ 204 | } while(0) 205 | 206 | #define utarray_sort(a,cmp) do { \ 207 | qsort((a)->d, (a)->i, (a)->icd.sz, cmp); \ 208 | } while(0) 209 | 210 | #define utarray_find(a,v,cmp) bsearch((v),(a)->d,(a)->i,(a)->icd.sz,cmp) 211 | 212 | #define utarray_front(a) (((a)->i) ? (_utarray_eltptr(a,0)) : NULL) 213 | #define utarray_next(a,e) (((e)==NULL) ? utarray_front(a) : ((((a)->i) > (utarray_eltidx(a,e)+1)) ? _utarray_eltptr(a,utarray_eltidx(a,e)+1) : NULL)) 214 | #define utarray_prev(a,e) (((e)==NULL) ? utarray_back(a) : ((utarray_eltidx(a,e) > 0) ? _utarray_eltptr(a,utarray_eltidx(a,e)-1) : NULL)) 215 | #define utarray_back(a) (((a)->i) ? (_utarray_eltptr(a,(a)->i-1)) : NULL) 216 | #define utarray_eltidx(a,e) (((char*)(e) >= (char*)((a)->d)) ? (((char*)(e) - (char*)((a)->d))/(size_t)(a)->icd.sz) : -1) 217 | 218 | /* last we pre-define a few icd for common utarrays of ints and strings */ 219 | static void utarray_str_cpy(void *dst, const void *src) { 220 | char **_src = (char**)src, **_dst = (char**)dst; 221 | *_dst = (*_src == NULL) ? NULL : strdup(*_src); 222 | } 223 | static void utarray_str_dtor(void *elt) { 224 | char **eltc = (char**)elt; 225 | if (*eltc) free(*eltc); 226 | } 227 | static const UT_icd ut_str_icd _UNUSED_ = {sizeof(char*),NULL,utarray_str_cpy,utarray_str_dtor}; 228 | static const UT_icd ut_int_icd _UNUSED_ = {sizeof(int),NULL,NULL,NULL}; 229 | static const UT_icd ut_ptr_icd _UNUSED_ = {sizeof(void*),NULL,NULL,NULL}; 230 | 231 | 232 | #endif /* UTARRAY_H */ 233 | -------------------------------------------------------------------------------- /owntracks-cli-publisher.c: -------------------------------------------------------------------------------- 1 | /* 2 | * OwnTracks CLI 3 | * Copyright (C) 2016-2020 Jan-Piet Mens 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU General Public License 7 | * as published by the Free Software Foundation; either version 2 8 | * of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include "json.h" 37 | #include "utarray.h" 38 | #include "utstring.h" 39 | #include "version.h" 40 | 41 | #define BLEN 8192 42 | 43 | #define QOS0 0 44 | #define QOS1 1 45 | #define QOS2 2 46 | #define SSL_VERIFY_PEER (1) 47 | #define SSL_VERIFY_NONE (0) 48 | 49 | #define max(a, b) ( (a > b) ? a : b ) 50 | 51 | #define PERIODIC_REPORT NULL 52 | #define ONDEMAND_REPORT "m" 53 | 54 | /* Globals, configured from environment */ 55 | FILE *fixlog = NULL; /* if open, record JSON to this file */ 56 | 57 | UT_array *parms; 58 | static struct gps_data_t gpsdata; 59 | long npubs = 0L; 60 | #if GPSD_API_MAJOR_VERSION >= 7 61 | char gpsmessage[BLEN]; 62 | size_t gpsmessagelen = BLEN; 63 | #endif 64 | 65 | #define PROGNAME "ocli" 66 | #define SIESTA 700 /* microseconds */ 67 | 68 | struct udata { 69 | struct mosquitto *mosq; 70 | char *clientid; 71 | char *basetopic; 72 | char *t_dump; /* topic on which to dump configuration to */ 73 | char *t_cmd; /* topic on which to subscribe to for commands */ 74 | char *tid; 75 | char *username; 76 | char *device; 77 | 78 | int interval; /* publish after seconds */ 79 | int displacement; /* publish after this number of meters movement (def: 0) */ 80 | bool verbose; 81 | }; 82 | 83 | void publish(struct udata *ud, char *topic, char *payload, int qos, bool retain); 84 | static void print_fix(struct udata *ud, struct gps_data_t *gpsdata, double time, char *reporttype); 85 | 86 | void catcher(int sig) 87 | { 88 | fprintf(stderr, "%s: Going down on signal %d\n", PROGNAME, sig); 89 | exit(1); 90 | } 91 | 92 | void publish(struct udata *ud, char *topic, char *payload, int qos, bool retain) 93 | { 94 | int rc; 95 | 96 | rc = mosquitto_publish(ud->mosq, NULL, topic, strlen(payload), payload, qos, retain); 97 | if (rc != MOSQ_ERR_SUCCESS) { 98 | fprintf(stderr, "%s: cannot publish: %s\n", PROGNAME, mosquitto_strerror(rc)); 99 | exit(1); 100 | } 101 | } 102 | 103 | void cb_connect(struct mosquitto *mosq, void *userdata, int reason) 104 | { 105 | struct udata *ud = (struct udata *)userdata; 106 | int rc; 107 | 108 | if ((rc = mosquitto_subscribe(ud->mosq, NULL, ud->t_cmd, QOS1)) != MOSQ_ERR_SUCCESS) { 109 | fprintf(stderr, "cannot subscribe to %s: %s\n", ud->t_cmd, 110 | mosquitto_strerror(rc)); 111 | } 112 | } 113 | 114 | void cb_disconnect(struct mosquitto *mosq, void *userdata, int reason) 115 | { 116 | if (reason == 0) { 117 | // Disconnect requested by client 118 | } else { 119 | fprintf(stderr, "%s: disconnected: reason: %d (%s)\n", 120 | PROGNAME, reason, strerror(errno)); 121 | sleep(2); 122 | } 123 | } 124 | 125 | static void config_dump(struct udata *ud) 126 | { 127 | char *json_string; 128 | JsonNode *jo; 129 | 130 | jo = json_mkobject(); 131 | 132 | json_append_member(jo, "_type", json_mkstring("configuration")); 133 | json_append_member(jo, "_npubs", json_mknumber(npubs)); // nonstandard 134 | json_append_member(jo, "clientId", json_mkstring(ud->clientid)); 135 | json_append_member(jo, "locatorInterval", json_mknumber(ud->interval)); 136 | json_append_member(jo, "locatorDisplacement", json_mknumber(ud->displacement)); 137 | json_append_member(jo, "pubTopicBase", json_mkstring(ud->basetopic)); 138 | if (ud->tid && *ud->tid) { 139 | json_append_member(jo, "tid", json_mkstring(ud->tid)); 140 | } 141 | json_append_member(jo, "username", json_mkstring(ud->username)); 142 | json_append_member(jo, "deviceId", json_mkstring(ud->device)); 143 | 144 | if ((json_string = json_stringify(jo, NULL)) != NULL) { 145 | publish(ud, ud->t_dump, json_string, QOS1, false); 146 | 147 | free(json_string); 148 | } 149 | 150 | json_delete(jo); 151 | } 152 | 153 | void set_config(struct udata *ud, JsonNode *json, char *payload) 154 | { 155 | JsonNode *conf, *j; 156 | 157 | /* FIXME: add checking for remoteConfiguration to permit changes */ 158 | 159 | if ((conf = json_find_member(json, "configuration")) == NULL) { 160 | fprintf(stderr, "No configuration in JSON %s\n", (char *)payload); 161 | return; 162 | } 163 | 164 | /* 165 | * We now have: 166 | * { 167 | * "_type": "configuration", 168 | * "bla": "foo" 169 | * } 170 | */ 171 | 172 | if (((j = json_find_member(conf, "_type")) == NULL) || 173 | (strcmp(j->string_, "configuration") != 0)) { 174 | fprintf(stderr, "No configuration in config action %s\n", payload); 175 | return; 176 | } 177 | 178 | json_foreach(j, conf) { 179 | if (strcmp(j->key, "_type") == 0) 180 | continue; 181 | // printf("%s\t%s\n", j->key, j->string_); 182 | 183 | if (strcmp(j->key, "locatorInterval") == 0) { 184 | ud->interval = j->number_; 185 | fprintf(stderr, "Set interval to %d\n", ud->interval); 186 | } else if (strcmp(j->key, "locatorDisplacement") == 0) { 187 | ud->displacement = j->number_; 188 | fprintf(stderr, "Set displacement to %d\n", ud->displacement); 189 | } else { 190 | fprintf(stderr, "Ignoring unknown configuration key `%s'\n", j->key); 191 | } 192 | } 193 | } 194 | 195 | void cb_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *msg) 196 | { 197 | struct udata *ud = (struct udata *)userdata; 198 | JsonNode *json, *j; 199 | char *action = NULL; 200 | 201 | if (msg->payloadlen < 2) 202 | return; 203 | 204 | if ((json = json_decode((char *)msg->payload)) == NULL) { 205 | fprintf(stderr, "Cannot decode JSON from %s\n", (char *)msg->payload); 206 | return; 207 | } 208 | 209 | if (((j = json_find_member(json, "_type")) == NULL) || 210 | (strcmp(j->string_, "cmd") != 0)) { 211 | fprintf(stderr, "No cmd in JSON %s\n", (char *)msg->payload); 212 | json_delete(json); 213 | return; 214 | } 215 | 216 | if ((j = json_find_member(json, "action")) == NULL) { 217 | fprintf(stderr, "No action in JSON %s\n", (char *)msg->payload); 218 | json_delete(json); 219 | return; 220 | } 221 | 222 | if (j->tag != JSON_STRING) { 223 | fprintf(stderr, "action tag is not string in JSON %s\n", (char *)msg->payload); 224 | json_delete(json); 225 | return; 226 | } 227 | 228 | action = strdup(j->string_); 229 | 230 | if (strcmp(action, "dump") == 0) { 231 | config_dump(ud); 232 | } else if (strcmp(action, "reportLocation") == 0) { 233 | print_fix(ud, &gpsdata, (time_t)time(0), ONDEMAND_REPORT); 234 | } else if (strcmp(action, "setConfiguration") == 0) { 235 | set_config(ud, json, (char *)msg->payload); 236 | } 237 | 238 | json_delete(json); 239 | if (action) 240 | free(action); 241 | } 242 | 243 | static void print_fix(struct udata *ud, struct gps_data_t *gpsdata, double ttime, char *reporttype) 244 | { 245 | char buf[128], *json_string, **p; 246 | double accuracy; 247 | struct gps_fix_t *fix = &(gpsdata->fix); 248 | JsonNode *jo; 249 | char tbuf[128]; 250 | 251 | if (isnan(fix->latitude) || 252 | isnan(fix->longitude) || 253 | isnan(ttime)) { 254 | return; 255 | } 256 | 257 | if (!isnan(fix->epx) && !isnan(fix->epy)) { 258 | accuracy = max(fix->epx, fix->epy); 259 | } else if (isnan(fix->epx) && !isnan(fix->epy)) { 260 | accuracy = fix->epy; 261 | } else if (!isnan(fix->epx) && isnan(fix->epx)) { 262 | accuracy = fix->epx; 263 | } else { 264 | accuracy = 0.0; 265 | } 266 | 267 | #if GPSD_API_MAJOR_VERSION >= 9 268 | timespec_t ts; 269 | ts.tv_sec = (long)ttime; 270 | ts.tv_nsec = 0; 271 | timespec_to_iso8601(ts, tbuf, sizeof(tbuf)); 272 | #else 273 | unix_to_iso8601(ttime, tbuf, sizeof(tbuf)); 274 | #endif 275 | if (ud->verbose) { 276 | printf("mode=%d, lat=%f, lon=%f, acc=%f, tst=%s (%ld)\n", 277 | fix->mode, 278 | fix->latitude, 279 | fix->longitude, 280 | accuracy, 281 | tbuf, 282 | (long)ttime); 283 | } 284 | 285 | jo = json_mkobject(); 286 | 287 | json_append_member(jo, "_type", json_mkstring("location")); 288 | json_append_member(jo, "lat", json_mknumber(fix->latitude)); 289 | json_append_member(jo, "lon", json_mknumber(fix->longitude)); 290 | json_append_member(jo, "acc", json_mknumber((long)accuracy)); 291 | json_append_member(jo, "tst", json_mknumber(ttime)); 292 | 293 | if (ud->tid && *ud->tid) { 294 | json_append_member(jo, "tid", json_mkstring(ud->tid)); 295 | } 296 | 297 | if (reporttype) { 298 | json_append_member(jo, "t", json_mkstring(reporttype)); 299 | } 300 | 301 | /* DEBUGGING */ 302 | sprintf(buf, "%d/%d", gpsdata->satellites_used, gpsdata->satellites_visible); 303 | json_append_member(jo, "sat", json_mkstring(buf)); 304 | if ((fix->mode == MODE_3D) && !isnan(fix->altitude)) { 305 | json_append_member(jo, "alt", json_mknumber((long)fix->altitude)); 306 | } 307 | if (!isnan(fix->speed)) { 308 | long kph = (fix->speed * 3.6); /* meters/s -> km/h */ 309 | json_append_member(jo, "vel", json_mknumber(kph)); 310 | } 311 | 312 | /* 313 | * For each of the filenames passed as parameters, use the basename 314 | * as key for the string value of the first line in the file and add 315 | * to the JSON. Skip if the key is already in the JSON object, i.e. 316 | * don't overwrite existing elements. 317 | */ 318 | 319 | p = NULL; 320 | while ((p = (char**)utarray_next(parms, p))) { 321 | char *key = basename(*p), *val = NULL; 322 | FILE *fp; 323 | bool is_exec = false; 324 | 325 | if (json_find_member(jo, key) != NULL) { 326 | // fprintf(stderr, "Refuse to overwrite key=%s\n", key); 327 | continue; 328 | } 329 | 330 | is_exec = access(*p, X_OK) == 0; 331 | 332 | if (is_exec) { 333 | fp = popen(*p, "r"); 334 | is_exec = true; 335 | } else { 336 | fp = fopen(*p, "r"); 337 | } 338 | 339 | char buf[1025], *bp; 340 | if (fp == NULL) { 341 | perror(*p); 342 | continue; 343 | } else { 344 | if (fgets(buf, sizeof(buf), fp) != NULL) { 345 | if ((bp = strchr(buf, '\r')) != NULL) 346 | *bp = 0; 347 | if ((bp = strchr(buf, '\n')) != NULL) 348 | *bp = 0; 349 | val = buf; 350 | } 351 | if (is_exec) { 352 | pclose(fp); 353 | } else { 354 | fclose(fp); 355 | } 356 | } 357 | if (val) { 358 | json_append_member(jo, key, json_mkstring(val)); 359 | } else { 360 | json_append_member(jo, key, json_mknull()); 361 | } 362 | } 363 | 364 | if ((json_string = json_stringify(jo, NULL)) != NULL) { 365 | npubs++; 366 | publish(ud, ud->basetopic, json_string, QOS1, true); 367 | 368 | if (fixlog) { 369 | fprintf(fixlog, "%s\n", json_string); 370 | fflush(fixlog); /* FIXME */ 371 | } 372 | free(json_string); 373 | } 374 | 375 | json_delete(jo); 376 | } 377 | 378 | 379 | static void conditionally_log_fix(struct udata *ud, struct gps_data_t *gpsdata) 380 | { 381 | struct gps_fix_t *fix = &(gpsdata->fix); 382 | #if GPSD_API_MAJOR_VERSION >= 9 383 | static timespec_t int_time, old_int_time; 384 | #else 385 | static double int_time, old_int_time; 386 | #endif 387 | static double old_lat, old_lon; 388 | static bool first = true; 389 | bool valid = false; 390 | 391 | if (gpsdata->set & POLICY_SET) { 392 | gpsdata->set &= ~(POLICY_SET); 393 | return; 394 | } 395 | 396 | if (gpsdata->set & STATUS_SET) { 397 | #if GPSD_API_MAJOR_VERSION >= 10 398 | switch (gpsdata->fix.status) { 399 | #else 400 | switch (gpsdata->status) { 401 | #endif 402 | #ifdef STATUS_FIX 403 | case STATUS_FIX: 404 | #endif 405 | #ifdef STATUS_GPS 406 | case STATUS_GPS: 407 | #endif 408 | #ifdef STATUS_DGPS_FIX 409 | case STATUS_DGPS_FIX: 410 | #endif 411 | #ifdef STATUS_DGPS 412 | case STATUS_DGPS: 413 | #endif 414 | switch (gpsdata->fix.mode) { 415 | case MODE_2D: 416 | if (gpsdata->set & LATLON_SET) { 417 | valid = true; 418 | } 419 | break; 420 | 421 | case MODE_3D: 422 | if (gpsdata->set & (LATLON_SET|ALTITUDE_SET)) { 423 | valid = true; 424 | } 425 | break; 426 | 427 | case MODE_NOT_SEEN: 428 | if (ud->verbose) { 429 | fprintf(stderr, ".. fix not yet seen\n"); 430 | } 431 | break; 432 | 433 | case MODE_NO_FIX: 434 | if (ud->verbose) { 435 | fprintf(stderr, ".. no fix yet\n"); 436 | } 437 | break; 438 | 439 | default: 440 | if (ud->verbose) { 441 | fprintf(stderr, ".. unpossible mode\n"); 442 | } 443 | break; 444 | } 445 | break; 446 | 447 | #ifdef STATUS_NO_FIX 448 | case STATUS_NO_FIX: 449 | #endif 450 | #ifdef STATUS_UNK 451 | case STATUS_UNK: 452 | #endif 453 | if (ud->verbose) { 454 | fprintf(stderr, ".. no fix\n"); 455 | } 456 | break; 457 | 458 | default: 459 | if (ud->verbose) { 460 | fprintf(stderr, "status == %llu\n", gpsdata->set & MODE_SET); 461 | } 462 | break; 463 | } 464 | } 465 | 466 | #if 0 467 | #if GPSD_API_MAJOR_VERSION >= 7 468 | char *bp; 469 | if ((bp = strchr(gpsmessage, '\r')) != NULL) 470 | *bp = 0; 471 | fprintf(stderr, "(%zu) %s\n", gpsmessagelen, gpsmessage); 472 | #endif 473 | #endif 474 | 475 | if (valid == false) 476 | return; 477 | 478 | int_time = fix->time; 479 | 480 | #if GPSD_API_MAJOR_VERSION >= 9 481 | if ((int_time.tv_sec == old_int_time.tv_sec) || fix->mode < MODE_2D) { 482 | #else 483 | if ((int_time == old_int_time) || fix->mode < MODE_2D) { 484 | #endif 485 | // puts("rubbish"); 486 | usleep(SIESTA); 487 | return; 488 | } 489 | 490 | /* may not be worth logging if we've moved only a very short distance */ 491 | if (ud->displacement>0 && !first && earth_distance( 492 | fix->latitude, 493 | fix->longitude, 494 | old_lat, old_lon) < ud->displacement) { 495 | // puts("not enough move"); 496 | usleep(SIESTA); 497 | return; 498 | } 499 | 500 | /* Don't log if interval seconds haven't elapsed since the last fix */ 501 | #if GPSD_API_MAJOR_VERSION >= 9 502 | if ((labs(int_time.tv_sec - old_int_time.tv_sec) < ud->interval) && !first) { 503 | #else 504 | if ((fabs(int_time - old_int_time) < ud->interval) && !first) { 505 | #endif 506 | // puts("too soon"); 507 | usleep(SIESTA); 508 | return; 509 | } 510 | 511 | if (first) 512 | first = false; 513 | 514 | old_int_time = int_time; 515 | if (ud->displacement > 0) { 516 | old_lat = fix->latitude; 517 | old_lon = fix->longitude; 518 | } 519 | 520 | #if GPSD_API_MAJOR_VERSION >= 9 521 | print_fix(ud, gpsdata, (double)int_time.tv_sec, PERIODIC_REPORT); 522 | #else 523 | print_fix(ud, gpsdata, int_time, PERIODIC_REPORT); 524 | #endif 525 | } 526 | 527 | static int env_number(char *key, int min) 528 | { 529 | char *p; 530 | int n; 531 | 532 | if ((p = getenv(key)) == NULL) 533 | return (min); 534 | if ((n = atoi(p)) < min) 535 | n = min; 536 | return (n); 537 | } 538 | 539 | void obsd_unveil(char *path, char *mode, char *comment) 540 | { 541 | #ifdef __OpenBSD__ 542 | if (unveil(path, mode) != 0) { 543 | fprintf(stderr, "%s: unveil(%s, %s) on %s: %s\n", 544 | PROGNAME, 545 | path ? path : "NULL", 546 | mode ? mode : "NULL", 547 | comment, 548 | strerror(errno)); 549 | exit(2); 550 | } 551 | #endif 552 | } 553 | 554 | int main(int argc, char **argv) 555 | { 556 | unsigned int flags = WATCH_NEWSTYLE; // WATCH_ENABLE | WATCH_JSON; 557 | int keepalive = 60, rc, c; 558 | char *p, *js; 559 | char *gpsd_host = "localhost", *gpsd_port = DEFAULT_GPSD_PORT; 560 | char *mqtt_host = "localhost"; 561 | short mqtt_port = 1883; 562 | char *mqtt_user = NULL; 563 | char *mqtt_pass = NULL; 564 | struct udata udata, *ud = &udata; 565 | char hostname[BUFSIZ], *h, *username; 566 | JsonNode *jo; 567 | char *cacert = NULL; 568 | 569 | ud->verbose = true; 570 | 571 | while ((c = getopt(argc, argv, "sv")) != EOF) { 572 | switch (c) { 573 | case 's': 574 | ud->verbose = false; 575 | break; 576 | case 'v': 577 | printf("ocli %s\n", VERSION); 578 | return (0); 579 | break; 580 | default: 581 | fprintf(stderr, "Usage: %s [-s] [-v] [parameter-file ..]\n", *argv); 582 | return (2); 583 | } 584 | } 585 | 586 | #ifdef __OpenBSD__ 587 | if (pledge("stdio inet unveil proc rpath exec", NULL) != 0) { 588 | perror("pledge"); 589 | exit(2); 590 | } 591 | #endif 592 | 593 | argc -= optind - 1; 594 | argv += optind - 1; 595 | 596 | ud->clientid = NULL; 597 | ud->interval = env_number("OCLI_INTERVAL", 1); // minsecs seconds 598 | ud->displacement = env_number("OCLI_DISPLACEMENT", 0); // minmove meters 599 | 600 | if ((p = getenv("GPSD_HOST")) != NULL) 601 | gpsd_host = strdup(p); 602 | 603 | if ((p = getenv("GPSD_PORT")) != NULL) { 604 | gpsd_port = atoi(p) < 1 ? DEFAULT_GPSD_PORT : p; 605 | } 606 | 607 | if ((p = getenv("MQTT_HOST")) != NULL) 608 | mqtt_host = strdup(p); 609 | 610 | if ((p = getenv("MQTT_PORT")) != NULL) { 611 | mqtt_port = atoi(p) < 1 ? 1883 : atoi(p); 612 | } 613 | 614 | if ((p = getenv("MQTT_USER")) != NULL) 615 | mqtt_user = strdup(p); 616 | 617 | if ((p = getenv("MQTT_PASS")) != NULL) 618 | mqtt_pass = strdup(p); 619 | 620 | if ((p = getenv("OCLI_CACERT")) != NULL) { 621 | cacert = strdup(p); 622 | obsd_unveil(cacert, "r", "$OCLI_CACERT"); 623 | } 624 | 625 | if ((p = getenv("fixlog")) != NULL) { 626 | fixlog = fopen(p, "a"); 627 | if (fixlog == NULL) { 628 | perror(p); 629 | exit(1); 630 | } 631 | obsd_unveil(p, "cw", "fixlog"); 632 | } 633 | 634 | utarray_new(parms, &ut_str_icd); 635 | while (*++argv) { 636 | /* No need to unveil() each *argv because we have rpath promise */ 637 | utarray_push_back(parms, &*argv); 638 | } 639 | 640 | obsd_unveil(NULL, NULL, "null veil"); 641 | 642 | if ((username = getlogin()) == NULL) 643 | username = "nobody"; 644 | 645 | if (gethostname(hostname, sizeof(hostname)) != 0) 646 | strcpy(hostname, "localhost"); 647 | 648 | if ((h = strchr(hostname, '.')) != NULL) 649 | *h = 0; 650 | 651 | /* 652 | * Build MQTT topic name which defaults to owntracks/user/device 653 | * but can be overriden from the environment. 654 | */ 655 | 656 | if ((p = getenv("BASE_TOPIC")) != NULL) { 657 | ud->basetopic = strdup(p); 658 | } else { 659 | UT_string *to; 660 | 661 | utstring_new(to); 662 | utstring_printf(to, "owntracks/%s/%s", username, hostname); 663 | ud->basetopic = strdup(utstring_body(to)); 664 | 665 | ud->username = strdup(username); 666 | ud->device = strdup(hostname); 667 | 668 | } 669 | ud->tid = getenv("OCLI_TID"); /* may be null */ 670 | 671 | if ((p = getenv("OCLI_CLIENTID")) != NULL) { 672 | ud->clientid = strdup(p); 673 | } else { 674 | UT_string *cid; 675 | 676 | utstring_new(cid); 677 | utstring_printf(cid, "ocli-%s-%s", username, hostname); 678 | ud->clientid = strdup(utstring_body(cid)); 679 | } 680 | 681 | ud->t_cmd = malloc(strlen(ud->basetopic) + strlen("/cmd") + 1); 682 | sprintf(ud->t_cmd, "%s/cmd", ud->basetopic); 683 | 684 | ud->t_dump = malloc(strlen(ud->basetopic) + strlen("/dump") + 1); 685 | sprintf(ud->t_dump, "%s/dump", ud->basetopic); 686 | 687 | if (ud->verbose) { 688 | printf("t_base %s\n", ud->basetopic); 689 | printf("t_cmd %s\n", ud->t_cmd); 690 | printf("t_dump %s\n", ud->t_dump ); 691 | printf("GPSD_API_MAJOR_VERSION %d\n", 692 | GPSD_API_MAJOR_VERSION); 693 | } 694 | 695 | mosquitto_lib_init(); 696 | 697 | ud->mosq = mosquitto_new(ud->clientid, true, (void *)&udata); 698 | if (!ud->mosq) { 699 | fprintf(stderr, "Out of memory.\n"); 700 | exit(1); 701 | } 702 | 703 | if (cacert != NULL) { 704 | rc = mosquitto_tls_set(ud->mosq, 705 | cacert, /* cafile */ 706 | NULL, /* capath */ 707 | NULL, /* certfile */ 708 | NULL, /* keyfile */ 709 | NULL /* pw_callback() */ 710 | ); 711 | if (rc != MOSQ_ERR_SUCCESS) { 712 | fprintf(stderr, "%s: cannot set TLS CA: %s (check path names)\n", 713 | PROGNAME, mosquitto_strerror(rc)); 714 | exit(3); 715 | } 716 | 717 | mosquitto_tls_opts_set(ud->mosq, 718 | SSL_VERIFY_PEER, 719 | NULL, /* tls_version: "tlsv1.2", "tlsv1" */ 720 | NULL /* ciphers */ 721 | ); 722 | } 723 | 724 | if (mqtt_user != NULL) { 725 | mosquitto_username_pw_set(ud->mosq, 726 | mqtt_user, 727 | mqtt_pass 728 | ); 729 | } 730 | 731 | /* Create payload for LWT consisting of starting timestamp */ 732 | jo = json_mkobject(); 733 | 734 | json_append_member(jo, "_type", json_mkstring("lwt")); 735 | json_append_member(jo, "tst", json_mknumber(time(0))); 736 | if ((js = json_stringify(jo, NULL)) != NULL) { 737 | if ((rc = mosquitto_will_set(ud->mosq, ud->basetopic, strlen(js), js, QOS1, true)) != MOSQ_ERR_SUCCESS) { 738 | fprintf(stderr, "Unable to set LWT: %s\n", mosquitto_strerror(rc)); 739 | } 740 | free(js); 741 | } 742 | json_delete(jo); 743 | 744 | mosquitto_connect_callback_set(ud->mosq, cb_connect); 745 | mosquitto_disconnect_callback_set(ud->mosq, cb_disconnect); 746 | mosquitto_message_callback_set(ud->mosq, cb_message); 747 | 748 | if ((rc = mosquitto_connect(ud->mosq, mqtt_host, mqtt_port, keepalive)) != MOSQ_ERR_SUCCESS) { 749 | fprintf(stderr, "Unable to connect to %s:%d: %s\n", mqtt_host, mqtt_port, 750 | mosquitto_strerror(rc)); 751 | perror(""); 752 | exit(2); 753 | } 754 | 755 | signal(SIGINT, catcher); 756 | signal(SIGTERM, catcher); 757 | 758 | mosquitto_loop_start(ud->mosq); 759 | 760 | if (gps_open(gpsd_host, gpsd_port, &gpsdata) != 0) { 761 | fprintf(stderr, 762 | "%s: no gpsd running or network error: %d, %s\n", 763 | argv[0], errno, gps_errstr(errno)); 764 | exit(EXIT_FAILURE); 765 | } 766 | 767 | gps_stream(&gpsdata, flags, NULL); 768 | 769 | while (1) { 770 | if (gps_waiting(&gpsdata, 5000000) == true) { 771 | errno = 0; 772 | #if GPSD_API_MAJOR_VERSION >= 7 773 | if (gps_read(&gpsdata, gpsmessage, gpsmessagelen) == -1) { 774 | #else 775 | if (gps_read(&gpsdata) == -1) { 776 | #endif 777 | if (errno == 0) { 778 | // lost contact with gpsd 779 | break; 780 | } 781 | } else { 782 | conditionally_log_fix(&udata, &gpsdata); 783 | } 784 | } 785 | } 786 | 787 | gps_stream(&gpsdata, WATCH_DISABLE, NULL); 788 | gps_close(&gpsdata); 789 | 790 | utarray_free(parms); 791 | 792 | mosquitto_disconnect(ud->mosq); 793 | mosquitto_loop_stop(ud->mosq, false); 794 | mosquitto_lib_cleanup(); 795 | 796 | exit(EXIT_SUCCESS); 797 | } 798 | -------------------------------------------------------------------------------- /json.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | #include "json.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #define out_of_memory() do { \ 33 | fprintf(stderr, "Out of memory.\n"); \ 34 | exit(EXIT_FAILURE); \ 35 | } while (0) 36 | 37 | /* Sadly, strdup is not portable. */ 38 | static char *json_strdup(const char *str) 39 | { 40 | char *ret = (char*) malloc(strlen(str) + 1); 41 | if (ret == NULL) 42 | out_of_memory(); 43 | strcpy(ret, str); 44 | return ret; 45 | } 46 | 47 | /* String buffer */ 48 | 49 | typedef struct 50 | { 51 | char *cur; 52 | char *end; 53 | char *start; 54 | } SB; 55 | 56 | static void sb_init(SB *sb) 57 | { 58 | sb->start = (char*) malloc(17); 59 | if (sb->start == NULL) 60 | out_of_memory(); 61 | sb->cur = sb->start; 62 | sb->end = sb->start + 16; 63 | } 64 | 65 | /* sb and need may be evaluated multiple times. */ 66 | #define sb_need(sb, need) do { \ 67 | if ((sb)->end - (sb)->cur < (need)) \ 68 | sb_grow(sb, need); \ 69 | } while (0) 70 | 71 | static void sb_grow(SB *sb, int need) 72 | { 73 | size_t length = sb->cur - sb->start; 74 | size_t alloc = sb->end - sb->start; 75 | 76 | do { 77 | alloc *= 2; 78 | } while (alloc < length + need); 79 | 80 | sb->start = (char*) realloc(sb->start, alloc + 1); 81 | if (sb->start == NULL) 82 | out_of_memory(); 83 | sb->cur = sb->start + length; 84 | sb->end = sb->start + alloc; 85 | } 86 | 87 | static void sb_put(SB *sb, const char *bytes, int count) 88 | { 89 | sb_need(sb, count); 90 | memcpy(sb->cur, bytes, count); 91 | sb->cur += count; 92 | } 93 | 94 | #define sb_putc(sb, c) do { \ 95 | if ((sb)->cur >= (sb)->end) \ 96 | sb_grow(sb, 1); \ 97 | *(sb)->cur++ = (c); \ 98 | } while (0) 99 | 100 | static void sb_puts(SB *sb, const char *str) 101 | { 102 | sb_put(sb, str, strlen(str)); 103 | } 104 | 105 | static char *sb_finish(SB *sb) 106 | { 107 | *sb->cur = 0; 108 | assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start)); 109 | return sb->start; 110 | } 111 | 112 | static void sb_free(SB *sb) 113 | { 114 | free(sb->start); 115 | } 116 | 117 | /* 118 | * Unicode helper functions 119 | * 120 | * These are taken from the ccan/charset module and customized a bit. 121 | * Putting them here means the compiler can (choose to) inline them, 122 | * and it keeps ccan/json from having a dependency. 123 | */ 124 | 125 | /* 126 | * Type for Unicode codepoints. 127 | * We need our own because wchar_t might be 16 bits. 128 | */ 129 | typedef uint32_t uchar_t; 130 | 131 | /* 132 | * Validate a single UTF-8 character starting at @s. 133 | * The string must be null-terminated. 134 | * 135 | * If it's valid, return its length (1 thru 4). 136 | * If it's invalid or clipped, return 0. 137 | * 138 | * This function implements the syntax given in RFC3629, which is 139 | * the same as that given in The Unicode Standard, Version 6.0. 140 | * 141 | * It has the following properties: 142 | * 143 | * * All codepoints U+0000..U+10FFFF may be encoded, 144 | * except for U+D800..U+DFFF, which are reserved 145 | * for UTF-16 surrogate pair encoding. 146 | * * UTF-8 byte sequences longer than 4 bytes are not permitted, 147 | * as they exceed the range of Unicode. 148 | * * The sixty-six Unicode "non-characters" are permitted 149 | * (namely, U+FDD0..U+FDEF, U+xxFFFE, and U+xxFFFF). 150 | */ 151 | static int utf8_validate_cz(const char *s) 152 | { 153 | unsigned char c = *s++; 154 | 155 | if (c <= 0x7F) { /* 00..7F */ 156 | return 1; 157 | } else if (c <= 0xC1) { /* 80..C1 */ 158 | /* Disallow overlong 2-byte sequence. */ 159 | return 0; 160 | } else if (c <= 0xDF) { /* C2..DF */ 161 | /* Make sure subsequent byte is in the range 0x80..0xBF. */ 162 | if (((unsigned char)*s++ & 0xC0) != 0x80) 163 | return 0; 164 | 165 | return 2; 166 | } else if (c <= 0xEF) { /* E0..EF */ 167 | /* Disallow overlong 3-byte sequence. */ 168 | if (c == 0xE0 && (unsigned char)*s < 0xA0) 169 | return 0; 170 | 171 | /* Disallow U+D800..U+DFFF. */ 172 | if (c == 0xED && (unsigned char)*s > 0x9F) 173 | return 0; 174 | 175 | /* Make sure subsequent bytes are in the range 0x80..0xBF. */ 176 | if (((unsigned char)*s++ & 0xC0) != 0x80) 177 | return 0; 178 | if (((unsigned char)*s++ & 0xC0) != 0x80) 179 | return 0; 180 | 181 | return 3; 182 | } else if (c <= 0xF4) { /* F0..F4 */ 183 | /* Disallow overlong 4-byte sequence. */ 184 | if (c == 0xF0 && (unsigned char)*s < 0x90) 185 | return 0; 186 | 187 | /* Disallow codepoints beyond U+10FFFF. */ 188 | if (c == 0xF4 && (unsigned char)*s > 0x8F) 189 | return 0; 190 | 191 | /* Make sure subsequent bytes are in the range 0x80..0xBF. */ 192 | if (((unsigned char)*s++ & 0xC0) != 0x80) 193 | return 0; 194 | if (((unsigned char)*s++ & 0xC0) != 0x80) 195 | return 0; 196 | if (((unsigned char)*s++ & 0xC0) != 0x80) 197 | return 0; 198 | 199 | return 4; 200 | } else { /* F5..FF */ 201 | return 0; 202 | } 203 | } 204 | 205 | /* Validate a null-terminated UTF-8 string. */ 206 | static bool utf8_validate(const char *s) 207 | { 208 | int len; 209 | 210 | for (; *s != 0; s += len) { 211 | len = utf8_validate_cz(s); 212 | if (len == 0) 213 | return false; 214 | } 215 | 216 | return true; 217 | } 218 | 219 | /* 220 | * Read a single UTF-8 character starting at @s, 221 | * returning the length, in bytes, of the character read. 222 | * 223 | * This function assumes input is valid UTF-8, 224 | * and that there are enough characters in front of @s. 225 | */ 226 | static int utf8_read_char(const char *s, uchar_t *out) 227 | { 228 | const unsigned char *c = (const unsigned char*) s; 229 | 230 | assert(utf8_validate_cz(s)); 231 | 232 | if (c[0] <= 0x7F) { 233 | /* 00..7F */ 234 | *out = c[0]; 235 | return 1; 236 | } else if (c[0] <= 0xDF) { 237 | /* C2..DF (unless input is invalid) */ 238 | *out = ((uchar_t)c[0] & 0x1F) << 6 | 239 | ((uchar_t)c[1] & 0x3F); 240 | return 2; 241 | } else if (c[0] <= 0xEF) { 242 | /* E0..EF */ 243 | *out = ((uchar_t)c[0] & 0xF) << 12 | 244 | ((uchar_t)c[1] & 0x3F) << 6 | 245 | ((uchar_t)c[2] & 0x3F); 246 | return 3; 247 | } else { 248 | /* F0..F4 (unless input is invalid) */ 249 | *out = ((uchar_t)c[0] & 0x7) << 18 | 250 | ((uchar_t)c[1] & 0x3F) << 12 | 251 | ((uchar_t)c[2] & 0x3F) << 6 | 252 | ((uchar_t)c[3] & 0x3F); 253 | return 4; 254 | } 255 | } 256 | 257 | /* 258 | * Write a single UTF-8 character to @s, 259 | * returning the length, in bytes, of the character written. 260 | * 261 | * @unicode must be U+0000..U+10FFFF, but not U+D800..U+DFFF. 262 | * 263 | * This function will write up to 4 bytes to @out. 264 | */ 265 | static int utf8_write_char(uchar_t unicode, char *out) 266 | { 267 | unsigned char *o = (unsigned char*) out; 268 | 269 | assert(unicode <= 0x10FFFF && !(unicode >= 0xD800 && unicode <= 0xDFFF)); 270 | 271 | if (unicode <= 0x7F) { 272 | /* U+0000..U+007F */ 273 | *o++ = unicode; 274 | return 1; 275 | } else if (unicode <= 0x7FF) { 276 | /* U+0080..U+07FF */ 277 | *o++ = 0xC0 | unicode >> 6; 278 | *o++ = 0x80 | (unicode & 0x3F); 279 | return 2; 280 | } else if (unicode <= 0xFFFF) { 281 | /* U+0800..U+FFFF */ 282 | *o++ = 0xE0 | unicode >> 12; 283 | *o++ = 0x80 | (unicode >> 6 & 0x3F); 284 | *o++ = 0x80 | (unicode & 0x3F); 285 | return 3; 286 | } else { 287 | /* U+10000..U+10FFFF */ 288 | *o++ = 0xF0 | unicode >> 18; 289 | *o++ = 0x80 | (unicode >> 12 & 0x3F); 290 | *o++ = 0x80 | (unicode >> 6 & 0x3F); 291 | *o++ = 0x80 | (unicode & 0x3F); 292 | return 4; 293 | } 294 | } 295 | 296 | /* 297 | * Compute the Unicode codepoint of a UTF-16 surrogate pair. 298 | * 299 | * @uc should be 0xD800..0xDBFF, and @lc should be 0xDC00..0xDFFF. 300 | * If they aren't, this function returns false. 301 | */ 302 | static bool from_surrogate_pair(uint16_t uc, uint16_t lc, uchar_t *unicode) 303 | { 304 | if (uc >= 0xD800 && uc <= 0xDBFF && lc >= 0xDC00 && lc <= 0xDFFF) { 305 | *unicode = 0x10000 + ((((uchar_t)uc & 0x3FF) << 10) | (lc & 0x3FF)); 306 | return true; 307 | } else { 308 | return false; 309 | } 310 | } 311 | 312 | /* 313 | * Construct a UTF-16 surrogate pair given a Unicode codepoint. 314 | * 315 | * @unicode must be U+10000..U+10FFFF. 316 | */ 317 | static void to_surrogate_pair(uchar_t unicode, uint16_t *uc, uint16_t *lc) 318 | { 319 | uchar_t n; 320 | 321 | assert(unicode >= 0x10000 && unicode <= 0x10FFFF); 322 | 323 | n = unicode - 0x10000; 324 | *uc = ((n >> 10) & 0x3FF) | 0xD800; 325 | *lc = (n & 0x3FF) | 0xDC00; 326 | } 327 | 328 | #define is_space(c) ((c) == '\t' || (c) == '\n' || (c) == '\r' || (c) == ' ') 329 | #define is_digit(c) ((c) >= '0' && (c) <= '9') 330 | 331 | static bool parse_value (const char **sp, JsonNode **out); 332 | static bool parse_string (const char **sp, char **out); 333 | static bool parse_number (const char **sp, double *out); 334 | static bool parse_array (const char **sp, JsonNode **out); 335 | static bool parse_object (const char **sp, JsonNode **out); 336 | static bool parse_hex16 (const char **sp, uint16_t *out); 337 | 338 | static bool expect_literal (const char **sp, const char *str); 339 | static void skip_space (const char **sp); 340 | 341 | static void emit_value (SB *out, const JsonNode *node); 342 | static void emit_value_indented (SB *out, const JsonNode *node, const char *space, int indent_level); 343 | static void emit_string (SB *out, const char *str); 344 | static void emit_number (SB *out, double num); 345 | static void emit_array (SB *out, const JsonNode *array); 346 | static void emit_array_indented (SB *out, const JsonNode *array, const char *space, int indent_level); 347 | static void emit_object (SB *out, const JsonNode *object); 348 | static void emit_object_indented (SB *out, const JsonNode *object, const char *space, int indent_level); 349 | 350 | static int write_hex16(char *out, uint16_t val); 351 | 352 | static JsonNode *mknode(JsonTag tag); 353 | static void append_node(JsonNode *parent, JsonNode *child); 354 | static void prepend_node(JsonNode *parent, JsonNode *child); 355 | static void append_member(JsonNode *object, char *key, JsonNode *value); 356 | 357 | /* Assertion-friendly validity checks */ 358 | static bool tag_is_valid(unsigned int tag); 359 | static bool number_is_valid(const char *num); 360 | 361 | JsonNode *json_decode(const char *json) 362 | { 363 | const char *s = json; 364 | JsonNode *ret; 365 | 366 | skip_space(&s); 367 | if (!parse_value(&s, &ret)) 368 | return NULL; 369 | 370 | skip_space(&s); 371 | if (*s != 0) { 372 | json_delete(ret); 373 | return NULL; 374 | } 375 | 376 | return ret; 377 | } 378 | 379 | char *json_encode(const JsonNode *node) 380 | { 381 | return json_stringify(node, NULL); 382 | } 383 | 384 | char *json_encode_string(const char *str) 385 | { 386 | SB sb; 387 | sb_init(&sb); 388 | 389 | emit_string(&sb, str); 390 | 391 | return sb_finish(&sb); 392 | } 393 | 394 | char *json_stringify(const JsonNode *node, const char *space) 395 | { 396 | SB sb; 397 | sb_init(&sb); 398 | 399 | if (space != NULL) 400 | emit_value_indented(&sb, node, space, 0); 401 | else 402 | emit_value(&sb, node); 403 | 404 | return sb_finish(&sb); 405 | } 406 | 407 | void json_delete(JsonNode *node) 408 | { 409 | if (node != NULL) { 410 | json_remove_from_parent(node); 411 | 412 | switch (node->tag) { 413 | case JSON_STRING: 414 | free(node->string_); 415 | break; 416 | case JSON_ARRAY: 417 | case JSON_OBJECT: 418 | { 419 | JsonNode *child, *next; 420 | for (child = node->children.head; child != NULL; child = next) { 421 | next = child->next; 422 | json_delete(child); 423 | } 424 | break; 425 | } 426 | default:; 427 | } 428 | 429 | free(node); 430 | } 431 | } 432 | 433 | bool json_validate(const char *json) 434 | { 435 | const char *s = json; 436 | 437 | skip_space(&s); 438 | if (!parse_value(&s, NULL)) 439 | return false; 440 | 441 | skip_space(&s); 442 | if (*s != 0) 443 | return false; 444 | 445 | return true; 446 | } 447 | 448 | JsonNode *json_find_element(JsonNode *array, int index) 449 | { 450 | JsonNode *element; 451 | int i = 0; 452 | 453 | if (array == NULL || array->tag != JSON_ARRAY) 454 | return NULL; 455 | 456 | json_foreach(element, array) { 457 | if (i == index) 458 | return element; 459 | i++; 460 | } 461 | 462 | return NULL; 463 | } 464 | 465 | JsonNode *json_find_member(JsonNode *object, const char *name) 466 | { 467 | JsonNode *member; 468 | 469 | if (object == NULL || object->tag != JSON_OBJECT) 470 | return NULL; 471 | 472 | json_foreach(member, object) 473 | if (strcmp(member->key, name) == 0) 474 | return member; 475 | 476 | return NULL; 477 | } 478 | 479 | JsonNode *json_first_child(const JsonNode *node) 480 | { 481 | if (node != NULL && (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT)) 482 | return node->children.head; 483 | return NULL; 484 | } 485 | 486 | static JsonNode *mknode(JsonTag tag) 487 | { 488 | JsonNode *ret = (JsonNode*) calloc(1, sizeof(JsonNode)); 489 | if (ret == NULL) 490 | out_of_memory(); 491 | ret->tag = tag; 492 | return ret; 493 | } 494 | 495 | JsonNode *json_mknull(void) 496 | { 497 | return mknode(JSON_NULL); 498 | } 499 | 500 | JsonNode *json_mkbool(bool b) 501 | { 502 | JsonNode *ret = mknode(JSON_BOOL); 503 | ret->bool_ = b; 504 | return ret; 505 | } 506 | 507 | static JsonNode *mkstring(char *s) 508 | { 509 | JsonNode *ret = mknode(JSON_STRING); 510 | ret->string_ = s; 511 | return ret; 512 | } 513 | 514 | JsonNode *json_mkstring(const char *s) 515 | { 516 | return mkstring(json_strdup(s)); 517 | } 518 | 519 | JsonNode *json_mknumber(double n) 520 | { 521 | JsonNode *node = mknode(JSON_NUMBER); 522 | node->number_ = n; 523 | return node; 524 | } 525 | 526 | JsonNode *json_mkarray(void) 527 | { 528 | return mknode(JSON_ARRAY); 529 | } 530 | 531 | JsonNode *json_mkobject(void) 532 | { 533 | return mknode(JSON_OBJECT); 534 | } 535 | 536 | static void append_node(JsonNode *parent, JsonNode *child) 537 | { 538 | child->parent = parent; 539 | child->prev = parent->children.tail; 540 | child->next = NULL; 541 | 542 | if (parent->children.tail != NULL) 543 | parent->children.tail->next = child; 544 | else 545 | parent->children.head = child; 546 | parent->children.tail = child; 547 | } 548 | 549 | static void prepend_node(JsonNode *parent, JsonNode *child) 550 | { 551 | child->parent = parent; 552 | child->prev = NULL; 553 | child->next = parent->children.head; 554 | 555 | if (parent->children.head != NULL) 556 | parent->children.head->prev = child; 557 | else 558 | parent->children.tail = child; 559 | parent->children.head = child; 560 | } 561 | 562 | static void append_member(JsonNode *object, char *key, JsonNode *value) 563 | { 564 | value->key = key; 565 | append_node(object, value); 566 | } 567 | 568 | void json_append_element(JsonNode *array, JsonNode *element) 569 | { 570 | assert(array->tag == JSON_ARRAY); 571 | assert(element->parent == NULL); 572 | 573 | append_node(array, element); 574 | } 575 | 576 | void json_prepend_element(JsonNode *array, JsonNode *element) 577 | { 578 | assert(array->tag == JSON_ARRAY); 579 | assert(element->parent == NULL); 580 | 581 | prepend_node(array, element); 582 | } 583 | 584 | void json_append_member(JsonNode *object, const char *key, JsonNode *value) 585 | { 586 | assert(object->tag == JSON_OBJECT); 587 | assert(value->parent == NULL); 588 | 589 | append_member(object, json_strdup(key), value); 590 | } 591 | 592 | void json_prepend_member(JsonNode *object, const char *key, JsonNode *value) 593 | { 594 | assert(object->tag == JSON_OBJECT); 595 | assert(value->parent == NULL); 596 | 597 | value->key = json_strdup(key); 598 | prepend_node(object, value); 599 | } 600 | 601 | void json_remove_from_parent(JsonNode *node) 602 | { 603 | JsonNode *parent = node->parent; 604 | 605 | if (parent != NULL) { 606 | if (node->prev != NULL) 607 | node->prev->next = node->next; 608 | else 609 | parent->children.head = node->next; 610 | if (node->next != NULL) 611 | node->next->prev = node->prev; 612 | else 613 | parent->children.tail = node->prev; 614 | 615 | free(node->key); 616 | 617 | node->parent = NULL; 618 | node->prev = node->next = NULL; 619 | node->key = NULL; 620 | } 621 | } 622 | 623 | static bool parse_value(const char **sp, JsonNode **out) 624 | { 625 | const char *s = *sp; 626 | 627 | switch (*s) { 628 | case 'n': 629 | if (expect_literal(&s, "null")) { 630 | if (out) 631 | *out = json_mknull(); 632 | *sp = s; 633 | return true; 634 | } 635 | return false; 636 | 637 | case 'f': 638 | if (expect_literal(&s, "false")) { 639 | if (out) 640 | *out = json_mkbool(false); 641 | *sp = s; 642 | return true; 643 | } 644 | return false; 645 | 646 | case 't': 647 | if (expect_literal(&s, "true")) { 648 | if (out) 649 | *out = json_mkbool(true); 650 | *sp = s; 651 | return true; 652 | } 653 | return false; 654 | 655 | case '"': { 656 | char *str; 657 | if (parse_string(&s, out ? &str : NULL)) { 658 | if (out) 659 | *out = mkstring(str); 660 | *sp = s; 661 | return true; 662 | } 663 | return false; 664 | } 665 | 666 | case '[': 667 | if (parse_array(&s, out)) { 668 | *sp = s; 669 | return true; 670 | } 671 | return false; 672 | 673 | case '{': 674 | if (parse_object(&s, out)) { 675 | *sp = s; 676 | return true; 677 | } 678 | return false; 679 | 680 | default: { 681 | double num; 682 | if (parse_number(&s, out ? &num : NULL)) { 683 | if (out) 684 | *out = json_mknumber(num); 685 | *sp = s; 686 | return true; 687 | } 688 | return false; 689 | } 690 | } 691 | } 692 | 693 | static bool parse_array(const char **sp, JsonNode **out) 694 | { 695 | const char *s = *sp; 696 | JsonNode *ret = out ? json_mkarray() : NULL; 697 | JsonNode *element; 698 | 699 | if (*s++ != '[') 700 | goto failure; 701 | skip_space(&s); 702 | 703 | if (*s == ']') { 704 | s++; 705 | goto success; 706 | } 707 | 708 | for (;;) { 709 | if (!parse_value(&s, out ? &element : NULL)) 710 | goto failure; 711 | skip_space(&s); 712 | 713 | if (out) 714 | json_append_element(ret, element); 715 | 716 | if (*s == ']') { 717 | s++; 718 | goto success; 719 | } 720 | 721 | if (*s++ != ',') 722 | goto failure; 723 | skip_space(&s); 724 | } 725 | 726 | success: 727 | *sp = s; 728 | if (out) 729 | *out = ret; 730 | return true; 731 | 732 | failure: 733 | json_delete(ret); 734 | return false; 735 | } 736 | 737 | static bool parse_object(const char **sp, JsonNode **out) 738 | { 739 | const char *s = *sp; 740 | JsonNode *ret = out ? json_mkobject() : NULL; 741 | char *key; 742 | JsonNode *value; 743 | 744 | if (*s++ != '{') 745 | goto failure; 746 | skip_space(&s); 747 | 748 | if (*s == '}') { 749 | s++; 750 | goto success; 751 | } 752 | 753 | for (;;) { 754 | if (!parse_string(&s, out ? &key : NULL)) 755 | goto failure; 756 | skip_space(&s); 757 | 758 | if (*s++ != ':') 759 | goto failure_free_key; 760 | skip_space(&s); 761 | 762 | if (!parse_value(&s, out ? &value : NULL)) 763 | goto failure_free_key; 764 | skip_space(&s); 765 | 766 | if (out) 767 | append_member(ret, key, value); 768 | 769 | if (*s == '}') { 770 | s++; 771 | goto success; 772 | } 773 | 774 | if (*s++ != ',') 775 | goto failure; 776 | skip_space(&s); 777 | } 778 | 779 | success: 780 | *sp = s; 781 | if (out) 782 | *out = ret; 783 | return true; 784 | 785 | failure_free_key: 786 | if (out) 787 | free(key); 788 | failure: 789 | json_delete(ret); 790 | return false; 791 | } 792 | 793 | bool parse_string(const char **sp, char **out) 794 | { 795 | const char *s = *sp; 796 | SB sb; 797 | char throwaway_buffer[4]; 798 | /* enough space for a UTF-8 character */ 799 | char *b; 800 | 801 | if (*s++ != '"') 802 | return false; 803 | 804 | if (out) { 805 | sb_init(&sb); 806 | sb_need(&sb, 4); 807 | b = sb.cur; 808 | } else { 809 | b = throwaway_buffer; 810 | } 811 | 812 | while (*s != '"') { 813 | unsigned char c = *s++; 814 | 815 | /* Parse next character, and write it to b. */ 816 | if (c == '\\') { 817 | c = *s++; 818 | switch (c) { 819 | case '"': 820 | case '\\': 821 | case '/': 822 | *b++ = c; 823 | break; 824 | case 'b': 825 | *b++ = '\b'; 826 | break; 827 | case 'f': 828 | *b++ = '\f'; 829 | break; 830 | case 'n': 831 | *b++ = '\n'; 832 | break; 833 | case 'r': 834 | *b++ = '\r'; 835 | break; 836 | case 't': 837 | *b++ = '\t'; 838 | break; 839 | case 'u': 840 | { 841 | uint16_t uc, lc; 842 | uchar_t unicode; 843 | 844 | if (!parse_hex16(&s, &uc)) 845 | goto failed; 846 | 847 | if (uc >= 0xD800 && uc <= 0xDFFF) { 848 | /* Handle UTF-16 surrogate pair. */ 849 | if (*s++ != '\\' || *s++ != 'u' || !parse_hex16(&s, &lc)) 850 | goto failed; /* Incomplete surrogate pair. */ 851 | if (!from_surrogate_pair(uc, lc, &unicode)) 852 | goto failed; /* Invalid surrogate pair. */ 853 | } else if (uc == 0) { 854 | /* Disallow "\u0000". */ 855 | goto failed; 856 | } else { 857 | unicode = uc; 858 | } 859 | 860 | b += utf8_write_char(unicode, b); 861 | break; 862 | } 863 | default: 864 | /* Invalid escape */ 865 | goto failed; 866 | } 867 | } else if (c <= 0x1F) { 868 | /* Control characters are not allowed in string literals. */ 869 | goto failed; 870 | } else { 871 | /* Validate and echo a UTF-8 character. */ 872 | int len; 873 | 874 | s--; 875 | len = utf8_validate_cz(s); 876 | if (len == 0) 877 | goto failed; /* Invalid UTF-8 character. */ 878 | 879 | while (len--) 880 | *b++ = *s++; 881 | } 882 | 883 | /* 884 | * Update sb to know about the new bytes, 885 | * and set up b to write another character. 886 | */ 887 | if (out) { 888 | sb.cur = b; 889 | sb_need(&sb, 4); 890 | b = sb.cur; 891 | } else { 892 | b = throwaway_buffer; 893 | } 894 | } 895 | s++; 896 | 897 | if (out) 898 | *out = sb_finish(&sb); 899 | *sp = s; 900 | return true; 901 | 902 | failed: 903 | if (out) 904 | sb_free(&sb); 905 | return false; 906 | } 907 | 908 | /* 909 | * The JSON spec says that a number shall follow this precise pattern 910 | * (spaces and quotes added for readability): 911 | * '-'? (0 | [1-9][0-9]*) ('.' [0-9]+)? ([Ee] [+-]? [0-9]+)? 912 | * 913 | * However, some JSON parsers are more liberal. For instance, PHP accepts 914 | * '.5' and '1.'. JSON.parse accepts '+3'. 915 | * 916 | * This function takes the strict approach. 917 | */ 918 | bool parse_number(const char **sp, double *out) 919 | { 920 | const char *s = *sp; 921 | 922 | /* '-'? */ 923 | if (*s == '-') 924 | s++; 925 | 926 | /* (0 | [1-9][0-9]*) */ 927 | if (*s == '0') { 928 | s++; 929 | } else { 930 | if (!is_digit(*s)) 931 | return false; 932 | do { 933 | s++; 934 | } while (is_digit(*s)); 935 | } 936 | 937 | /* ('.' [0-9]+)? */ 938 | if (*s == '.') { 939 | s++; 940 | if (!is_digit(*s)) 941 | return false; 942 | do { 943 | s++; 944 | } while (is_digit(*s)); 945 | } 946 | 947 | /* ([Ee] [+-]? [0-9]+)? */ 948 | if (*s == 'E' || *s == 'e') { 949 | s++; 950 | if (*s == '+' || *s == '-') 951 | s++; 952 | if (!is_digit(*s)) 953 | return false; 954 | do { 955 | s++; 956 | } while (is_digit(*s)); 957 | } 958 | 959 | if (out) 960 | *out = strtod(*sp, NULL); 961 | 962 | *sp = s; 963 | return true; 964 | } 965 | 966 | static void skip_space(const char **sp) 967 | { 968 | const char *s = *sp; 969 | while (is_space(*s)) 970 | s++; 971 | *sp = s; 972 | } 973 | 974 | static void emit_value(SB *out, const JsonNode *node) 975 | { 976 | assert(tag_is_valid(node->tag)); 977 | switch (node->tag) { 978 | case JSON_NULL: 979 | sb_puts(out, "null"); 980 | break; 981 | case JSON_BOOL: 982 | sb_puts(out, node->bool_ ? "true" : "false"); 983 | break; 984 | case JSON_STRING: 985 | emit_string(out, node->string_); 986 | break; 987 | case JSON_NUMBER: 988 | emit_number(out, node->number_); 989 | break; 990 | case JSON_ARRAY: 991 | emit_array(out, node); 992 | break; 993 | case JSON_OBJECT: 994 | emit_object(out, node); 995 | break; 996 | default: 997 | assert(false); 998 | } 999 | } 1000 | 1001 | void emit_value_indented(SB *out, const JsonNode *node, const char *space, int indent_level) 1002 | { 1003 | assert(tag_is_valid(node->tag)); 1004 | switch (node->tag) { 1005 | case JSON_NULL: 1006 | sb_puts(out, "null"); 1007 | break; 1008 | case JSON_BOOL: 1009 | sb_puts(out, node->bool_ ? "true" : "false"); 1010 | break; 1011 | case JSON_STRING: 1012 | emit_string(out, node->string_); 1013 | break; 1014 | case JSON_NUMBER: 1015 | emit_number(out, node->number_); 1016 | break; 1017 | case JSON_ARRAY: 1018 | emit_array_indented(out, node, space, indent_level); 1019 | break; 1020 | case JSON_OBJECT: 1021 | emit_object_indented(out, node, space, indent_level); 1022 | break; 1023 | default: 1024 | assert(false); 1025 | } 1026 | } 1027 | 1028 | static void emit_array(SB *out, const JsonNode *array) 1029 | { 1030 | const JsonNode *element; 1031 | 1032 | sb_putc(out, '['); 1033 | json_foreach(element, array) { 1034 | emit_value(out, element); 1035 | if (element->next != NULL) 1036 | sb_putc(out, ','); 1037 | } 1038 | sb_putc(out, ']'); 1039 | } 1040 | 1041 | static void emit_array_indented(SB *out, const JsonNode *array, const char *space, int indent_level) 1042 | { 1043 | const JsonNode *element = array->children.head; 1044 | int i; 1045 | 1046 | if (element == NULL) { 1047 | sb_puts(out, "[]"); 1048 | return; 1049 | } 1050 | 1051 | sb_puts(out, "[\n"); 1052 | while (element != NULL) { 1053 | for (i = 0; i < indent_level + 1; i++) 1054 | sb_puts(out, space); 1055 | emit_value_indented(out, element, space, indent_level + 1); 1056 | 1057 | element = element->next; 1058 | sb_puts(out, element != NULL ? ",\n" : "\n"); 1059 | } 1060 | for (i = 0; i < indent_level; i++) 1061 | sb_puts(out, space); 1062 | sb_putc(out, ']'); 1063 | } 1064 | 1065 | static void emit_object(SB *out, const JsonNode *object) 1066 | { 1067 | const JsonNode *member; 1068 | 1069 | sb_putc(out, '{'); 1070 | json_foreach(member, object) { 1071 | emit_string(out, member->key); 1072 | sb_putc(out, ':'); 1073 | emit_value(out, member); 1074 | if (member->next != NULL) 1075 | sb_putc(out, ','); 1076 | } 1077 | sb_putc(out, '}'); 1078 | } 1079 | 1080 | static void emit_object_indented(SB *out, const JsonNode *object, const char *space, int indent_level) 1081 | { 1082 | const JsonNode *member = object->children.head; 1083 | int i; 1084 | 1085 | if (member == NULL) { 1086 | sb_puts(out, "{}"); 1087 | return; 1088 | } 1089 | 1090 | sb_puts(out, "{\n"); 1091 | while (member != NULL) { 1092 | for (i = 0; i < indent_level + 1; i++) 1093 | sb_puts(out, space); 1094 | emit_string(out, member->key); 1095 | sb_puts(out, ": "); 1096 | emit_value_indented(out, member, space, indent_level + 1); 1097 | 1098 | member = member->next; 1099 | sb_puts(out, member != NULL ? ",\n" : "\n"); 1100 | } 1101 | for (i = 0; i < indent_level; i++) 1102 | sb_puts(out, space); 1103 | sb_putc(out, '}'); 1104 | } 1105 | 1106 | void emit_string(SB *out, const char *str) 1107 | { 1108 | bool escape_unicode = false; 1109 | const char *s = str; 1110 | char *b; 1111 | 1112 | assert(utf8_validate(str)); 1113 | 1114 | /* 1115 | * 14 bytes is enough space to write up to two 1116 | * \uXXXX escapes and two quotation marks. 1117 | */ 1118 | sb_need(out, 14); 1119 | b = out->cur; 1120 | 1121 | *b++ = '"'; 1122 | while (*s != 0) { 1123 | unsigned char c = *s++; 1124 | 1125 | /* Encode the next character, and write it to b. */ 1126 | switch (c) { 1127 | case '"': 1128 | *b++ = '\\'; 1129 | *b++ = '"'; 1130 | break; 1131 | case '\\': 1132 | *b++ = '\\'; 1133 | *b++ = '\\'; 1134 | break; 1135 | case '\b': 1136 | *b++ = '\\'; 1137 | *b++ = 'b'; 1138 | break; 1139 | case '\f': 1140 | *b++ = '\\'; 1141 | *b++ = 'f'; 1142 | break; 1143 | case '\n': 1144 | *b++ = '\\'; 1145 | *b++ = 'n'; 1146 | break; 1147 | case '\r': 1148 | *b++ = '\\'; 1149 | *b++ = 'r'; 1150 | break; 1151 | case '\t': 1152 | *b++ = '\\'; 1153 | *b++ = 't'; 1154 | break; 1155 | default: { 1156 | int len; 1157 | 1158 | s--; 1159 | len = utf8_validate_cz(s); 1160 | 1161 | if (len == 0) { 1162 | /* 1163 | * Handle invalid UTF-8 character gracefully in production 1164 | * by writing a replacement character (U+FFFD) 1165 | * and skipping a single byte. 1166 | * 1167 | * This should never happen when assertions are enabled 1168 | * due to the assertion at the beginning of this function. 1169 | */ 1170 | assert(false); 1171 | if (escape_unicode) { 1172 | strcpy(b, "\\uFFFD"); 1173 | b += 6; 1174 | } else { 1175 | *b++ = 0xEF; 1176 | *b++ = 0xBF; 1177 | *b++ = 0xBD; 1178 | } 1179 | s++; 1180 | } else if (c < 0x1F || (c >= 0x80 && escape_unicode)) { 1181 | /* Encode using \u.... */ 1182 | uint32_t unicode; 1183 | 1184 | s += utf8_read_char(s, &unicode); 1185 | 1186 | if (unicode <= 0xFFFF) { 1187 | *b++ = '\\'; 1188 | *b++ = 'u'; 1189 | b += write_hex16(b, unicode); 1190 | } else { 1191 | /* Produce a surrogate pair. */ 1192 | uint16_t uc, lc; 1193 | assert(unicode <= 0x10FFFF); 1194 | to_surrogate_pair(unicode, &uc, &lc); 1195 | *b++ = '\\'; 1196 | *b++ = 'u'; 1197 | b += write_hex16(b, uc); 1198 | *b++ = '\\'; 1199 | *b++ = 'u'; 1200 | b += write_hex16(b, lc); 1201 | } 1202 | } else { 1203 | /* Write the character directly. */ 1204 | while (len--) 1205 | *b++ = *s++; 1206 | } 1207 | 1208 | break; 1209 | } 1210 | } 1211 | 1212 | /* 1213 | * Update *out to know about the new bytes, 1214 | * and set up b to write another encoded character. 1215 | */ 1216 | out->cur = b; 1217 | sb_need(out, 14); 1218 | b = out->cur; 1219 | } 1220 | *b++ = '"'; 1221 | 1222 | out->cur = b; 1223 | } 1224 | 1225 | static void emit_number(SB *out, double num) 1226 | { 1227 | /* 1228 | * This isn't exactly how JavaScript renders numbers, 1229 | * but it should produce valid JSON for reasonable numbers 1230 | * preserve precision well enough, and avoid some oddities 1231 | * like 0.3 -> 0.299999999999999988898 . 1232 | */ 1233 | char buf[64]; 1234 | sprintf(buf, "%.16g", num); 1235 | 1236 | if (number_is_valid(buf)) 1237 | sb_puts(out, buf); 1238 | else 1239 | sb_puts(out, "null"); 1240 | } 1241 | 1242 | static bool tag_is_valid(unsigned int tag) 1243 | { 1244 | return (/* tag >= JSON_NULL && */ tag <= JSON_OBJECT); 1245 | } 1246 | 1247 | static bool number_is_valid(const char *num) 1248 | { 1249 | return (parse_number(&num, NULL) && *num == '\0'); 1250 | } 1251 | 1252 | static bool expect_literal(const char **sp, const char *str) 1253 | { 1254 | const char *s = *sp; 1255 | 1256 | while (*str != '\0') 1257 | if (*s++ != *str++) 1258 | return false; 1259 | 1260 | *sp = s; 1261 | return true; 1262 | } 1263 | 1264 | /* 1265 | * Parses exactly 4 hex characters (capital or lowercase). 1266 | * Fails if any input chars are not [0-9A-Fa-f]. 1267 | */ 1268 | static bool parse_hex16(const char **sp, uint16_t *out) 1269 | { 1270 | const char *s = *sp; 1271 | uint16_t ret = 0; 1272 | uint16_t i; 1273 | uint16_t tmp; 1274 | char c; 1275 | 1276 | for (i = 0; i < 4; i++) { 1277 | c = *s++; 1278 | if (c >= '0' && c <= '9') 1279 | tmp = c - '0'; 1280 | else if (c >= 'A' && c <= 'F') 1281 | tmp = c - 'A' + 10; 1282 | else if (c >= 'a' && c <= 'f') 1283 | tmp = c - 'a' + 10; 1284 | else 1285 | return false; 1286 | 1287 | ret <<= 4; 1288 | ret += tmp; 1289 | } 1290 | 1291 | if (out) 1292 | *out = ret; 1293 | *sp = s; 1294 | return true; 1295 | } 1296 | 1297 | /* 1298 | * Encodes a 16-bit number into hexadecimal, 1299 | * writing exactly 4 hex chars. 1300 | */ 1301 | static int write_hex16(char *out, uint16_t val) 1302 | { 1303 | const char *hex = "0123456789ABCDEF"; 1304 | 1305 | *out++ = hex[(val >> 12) & 0xF]; 1306 | *out++ = hex[(val >> 8) & 0xF]; 1307 | *out++ = hex[(val >> 4) & 0xF]; 1308 | *out++ = hex[ val & 0xF]; 1309 | 1310 | return 4; 1311 | } 1312 | 1313 | bool json_check(const JsonNode *node, char errmsg[256]) 1314 | { 1315 | #define problem(...) do { \ 1316 | if (errmsg != NULL) \ 1317 | snprintf(errmsg, 256, __VA_ARGS__); \ 1318 | return false; \ 1319 | } while (0) 1320 | 1321 | if (node->key != NULL && !utf8_validate(node->key)) 1322 | problem("key contains invalid UTF-8"); 1323 | 1324 | if (!tag_is_valid(node->tag)) 1325 | problem("tag is invalid (%u)", node->tag); 1326 | 1327 | if (node->tag == JSON_BOOL) { 1328 | if (node->bool_ != false && node->bool_ != true) 1329 | problem("bool_ is neither false (%d) nor true (%d)", (int)false, (int)true); 1330 | } else if (node->tag == JSON_STRING) { 1331 | if (node->string_ == NULL) 1332 | problem("string_ is NULL"); 1333 | if (!utf8_validate(node->string_)) 1334 | problem("string_ contains invalid UTF-8"); 1335 | } else if (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT) { 1336 | JsonNode *head = node->children.head; 1337 | JsonNode *tail = node->children.tail; 1338 | 1339 | if (head == NULL || tail == NULL) { 1340 | if (head != NULL) 1341 | problem("tail is NULL, but head is not"); 1342 | if (tail != NULL) 1343 | problem("head is NULL, but tail is not"); 1344 | } else { 1345 | JsonNode *child; 1346 | JsonNode *last = NULL; 1347 | 1348 | if (head->prev != NULL) 1349 | problem("First child's prev pointer is not NULL"); 1350 | 1351 | for (child = head; child != NULL; last = child, child = child->next) { 1352 | if (child == node) 1353 | problem("node is its own child"); 1354 | if (child->next == child) 1355 | problem("child->next == child (cycle)"); 1356 | if (child->next == head) 1357 | problem("child->next == head (cycle)"); 1358 | 1359 | if (child->parent != node) 1360 | problem("child does not point back to parent"); 1361 | if (child->next != NULL && child->next->prev != child) 1362 | problem("child->next does not point back to child"); 1363 | 1364 | if (node->tag == JSON_ARRAY && child->key != NULL) 1365 | problem("Array element's key is not NULL"); 1366 | if (node->tag == JSON_OBJECT && child->key == NULL) 1367 | problem("Object member's key is NULL"); 1368 | 1369 | if (!json_check(child, errmsg)) 1370 | return false; 1371 | } 1372 | 1373 | if (last != tail) 1374 | problem("tail does not match pointer found by starting at head and following next links"); 1375 | } 1376 | } 1377 | 1378 | return true; 1379 | 1380 | #undef problem 1381 | } 1382 | --------------------------------------------------------------------------------