{"id":24664,"date":"2020-05-05T10:11:00","date_gmt":"2020-05-05T08:11:00","guid":{"rendered":"https:\/\/pi3g.com\/?p=24664"},"modified":"2020-05-05T10:11:00","modified_gmt":"2020-05-05T08:11:00","slug":"termishell-a-web-based-shell-for-the-raspberry-pi-development-notes","status":"publish","type":"post","link":"https:\/\/pi3g.com\/de\/termishell-a-web-based-shell-for-the-raspberry-pi-development-notes\/","title":{"rendered":"TermiShell: eine webbasierte Shell f\u00fcr den Raspberry Pi (Entwicklungsnotizen)"},"content":{"rendered":"<h1>Introduction<\/h1>\n<p>In the course of development of <a href=\"https:\/\/www.picockpit.com\">PiCockpit<\/a>, I am going to add a web-based Terminal called <strong>TermiShell<\/strong>.<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"351\" height=\"264\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb.jpg\" border=\"0\"><\/a><\/p>\n<p><em>TermiShell icon, by: <\/em><a href=\"https:\/\/unsplash.com\/@stephanieharvey?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText\"><em>Stephanie Harvey<\/em><\/a><em> via unsplash.com <\/em><\/p>\n<p>TermiShell is going to allow you to log into your Raspberry Pi using PiCockpit.com (and the picockpit-client) \u2013 no additional application required on either side. This should be very comfortable, especially when on the go.<\/p>\n<p><strong>TermiShell is not going to be released in the upcoming v2.0 release of PiCockpit<\/strong>, because it requires additional preparation work, and would delay the upcoming release significantly. I would also rather not cut corners on security (which would be the alternative to make it work right away). <\/p>\n<p>The work, however, will positively impact many other capabilities of PiCockpit as well \u2013 for instance the ability to stream video (from the Pi camera), file uploads \/ downloads from the Pi, and many other functionalities which require data streams.<\/p>\n<p>I am compiling information for myself, and also for other interested developers about my thoughts on how to realize such a web-based terminal, and background information.<\/p>\n<h1>What is a pseudo-terminal (pty)?<\/h1>\n<p>Python offers built-in functionalities to execute processes, and to capture their output (stdout and stderr) and send them input (stdin).<\/p>\n<p>Here is a first attempt with subprocess:<\/p>\n<blockquote>\n<p># dow, day of work 1.5.2020<br \/>\nimport os<br \/>\nimport sys<br \/>\nimport time<br \/>\nimport threading<br \/>\nimport contextlib<br \/>\nimport subprocess<\/p>\n<p>print(&#8220;Hello world!&#8221;)<br \/>\nprint(&#8220;Running omxplayer&#8221;)<\/p>\n<p>\ndef output_reader(proc):<br \/>&nbsp;&nbsp;&nbsp;&nbsp; contFlag = True<br \/>&nbsp;&nbsp;&nbsp;&nbsp; while contFlag:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; chars = proc.stdout.read(1)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if chars != None:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if chars != b&#8221;:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(chars.decode(&#8216;utf-8&#8217;), end=&#8221;&#8221;, flush=True)<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else:<br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; contFlag = False<\/p>\n<p>\nproc = subprocess.Popen(<br \/>&nbsp;&nbsp;&nbsp;&nbsp; [&#8216;omxplayer&#8217;, &#8216;\/home\/pi\/thais.mp4&#8217;],<br \/>&nbsp;&nbsp;&nbsp;&nbsp; stdin=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp; stdout=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp; stderr=subprocess.PIPE,<br \/>&nbsp;&nbsp;&nbsp;&nbsp; bufsize=0)<\/p>\n<p>t = threading.Thread(target=output_reader, args=(proc,))<br \/>\nt.start()<\/p>\n<p>sys.stdin = os.fdopen(sys.stdin.fileno(), &#8216;rb&#8217;, buffering=0)<\/p>\n<p>while True:<br \/>&nbsp;&nbsp;&nbsp;&nbsp; char = sys.stdin.read(1)<br \/>&nbsp;&nbsp;&nbsp;&nbsp; print(char.decode(&#8216;utf-8&#8217;), end=&#8221;&#8221;, flush=True)<br \/>&nbsp;&nbsp;&nbsp;&nbsp; proc.stdin.write(char)<\/p>\n<p>proc.stdin.write(b&#8217;z&#8217;)<\/p>\n<p>t.join()<\/p>\n<\/blockquote>\n<p>Please note the following in the code above (which is not perfect, just demo code \u2013 and in fact might not work as expected!):<\/p>\n<ul>\n<li>bufsize is set to 0 \u2013 to suppress buffering<\/li>\n<li>I set up a thread to read the output of the process character by character<\/li>\n<li>I set up stdin to have zero buffering. This in turn requires it to be opened in binary mode (rb)<\/li>\n<li>I read characters one at a time from the user\u2019s input. I echo them using flush=True (otherwise output is line-buffered in Python by default for print)<\/li>\n<li>I write the character to the process. Remember, <strong>it is not buffered because we set up bufsize=0<\/strong><\/li>\n<\/ul>\n<p>Even so, we run into the following situation: output from the application (in this case omxplayer), is not received character by character, as expected \u2013 rather it is dumped all at once, at exit.<\/p>\n<p>Even though buffering is set to 0. Why?<\/p>\n<h2>buffering &amp; interactive processes<\/h2>\n<p>Linux stdio buffers. It is clever at this, too. If the process is <strong>not<\/strong> connected to an interactive process (terminal), but to a pipe, <strong>output is buffered<\/strong> until the buffer has been filled.<\/p>\n<p>Then it is efficiently copied over to the other application. <\/p>\n<p>This is good, resource-efficient behavior for a lot of use cases. But not if you are trying to interactively control the application. <\/p>\n<p>There is also nothing you, the developer, can do to influence the behavior of the other application when talking to you through a pipe. <\/p>\n<p>You would need to recompile the other application (and manually adjust the buffering behavior).<\/p>\n<p>Here are some further resources on this topic:<\/p>\n<ul>\n<li><a href=\"http:\/\/www.pixelbeat.org\/programming\/stdio_buffering\/\">stdio buffering<\/a><\/li>\n<li><a href=\"https:\/\/unix.stackexchange.com\/questions\/25372\/turn-off-buffering-in-pipe\">stackexchange turn off buffering in pipe<\/a><\/li>\n<\/ul>\n<p>Humans \u2013 and applications locking if no output is provided immediately &#8211; obviously need interactive output. This is where the pseudoterminal comes in.<\/p>\n<h2>The pseudoterminal<\/h2>\n<p>The pseudoterminal simulates an interactive terminal to the application. stdio thinks it is talking to a human, and does not buffer. Output is as you would expect it from interacting with Linux applications on the command line (e.g. via SSH).<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image.png\"><img loading=\"lazy\" decoding=\"async\" width=\"845\" height=\"109\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb.png\" border=\"0\"><\/a><\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-1.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1548\" height=\"55\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-1.png\" border=\"0\"><\/a><\/p>\n<p>As you can see in the output of ps aux, some applications do not have a TTY (terminal) assigned to them (showing up with a question mark \u201c?\u201d) \u2013 in this case, expect the applications to show a different default buffering behavior.<\/p>\n<p>Pseudoterminals look like this in ps aux:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-2.png\"><img loading=\"lazy\" decoding=\"async\" width=\"938\" height=\"436\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-2.png\" border=\"0\"><\/a><\/p>\n<p>I will decode the information for you:<\/p>\n<ul>\n<li>sshd connects to pseudoterminal 0 (pts\/0). <\/li>\n<li>bash, and some other processes are started on pseudoterminal 0 (pts\/0)<\/li>\n<li>I use sudo su to run a command as root (which in turn runs su, and then bash): python3 ttt.py<\/li>\n<li>this script (which I will show you in a little while) <strong>creates <\/strong>a new pseudoterminal pts\/1<\/li>\n<li>I run <strong>\/bin\/login <\/strong>from my script to check the user credentials. Because I entered them correctly, bash (the default shell) is started on pts\/1<\/li>\n<li>here I ping miau.de \u2013 this process is also executed in pts\/1<\/li>\n<li>I also start a second SSH connection, which attaches to pts\/2 \u2013 in this case to run ps aux, to be able to create the screenshot above.<\/li>\n<\/ul>\n<p>This is what the first SSH connection looks like:<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-3.png\"><img loading=\"lazy\" decoding=\"async\" width=\"564\" height=\"391\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-3.png\" border=\"0\"><\/a><\/p>\n<p><em>(Note to the astute reader: I tried two times, hence first the ping to google)<\/em><\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-4.png\"><img loading=\"lazy\" decoding=\"async\" width=\"570\" height=\"325\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-4.png\" border=\"0\"><\/a><\/p>\n<p>Further reading:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.linusakesson.net\/programming\/tty\/\">the TTY demystified<\/a><\/li>\n<\/ul>\n<h1>Python backend &amp; more on pseudoterminals<\/h1>\n<p>Python has built-in utilities for the pseudo-terminal: <a href=\"https:\/\/docs.python.org\/3\/library\/pty.html\">pty<\/a>. I found the documentation to be hard to get into, what a child \/ master \/ slave refer to. <\/p>\n<p>I found an example <a href=\"https:\/\/github.com\/pyinvoke\/invoke\/blob\/41cacac3b8c1499af4a219ce7ca3dfac96cc5790\/invoke\/runners.py\">here<\/a>, in the&nbsp; <a href=\"http:\/\/www.pyinvoke.org\/\">Invoke<\/a> source code. The \u201cgood stuff\u201d starts in line 1242 (def start \u2013 no pun intended <img decoding=\"async\" class=\"wlEmoticon wlEmoticon-smile\" style=\"\" alt=\"Smile\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/wlEmoticon-smile.png\">). As you can see, there is a reference to pexpect.spawn doing additional stuff for setup and tear down.<\/p>\n<p>Therefore, I simply decided to use <a href=\"https:\/\/pexpect.readthedocs.io\/en\/stable\/\"><strong>pexpect<\/strong><\/a> as a wrapper library.<\/p>\n<h2>pexpect<\/h2>\n<p>The documentation for pexpect <a href=\"https:\/\/pexpect.readthedocs.io\/en\/stable\/\">can be found here<\/a>. It\u2019s main use case is to automate interactive applications. <\/p>\n<p>Think, for instance, a command line FTP client. It can also be used to automate tests of applications. <\/p>\n<p>pexpect creates a pseudoterminal for the applications it launches. <\/p>\n<p>As discussed above, the pseudoterminal&nbsp; has the big and required advantage of putting these applications into a different buffering mode, which we are used to from interactive applications. <\/p>\n<p>Pseudoterminals behave like real terminals \u2013 it has a screen size (number of columns and rows), and applications write control sequences to it to affect the display. <\/p>\n<h2>Screen size and SIGWINCH<\/h2>\n<p>Linus Akesson <a href=\"https:\/\/www.linusakesson.net\/programming\/tty\/\">has a good explanation on TTYs in general<\/a>, and also about SIGWINCH. SIGWINCH is a signal, similar to SIGHUP or SIGINT. <\/p>\n<p>In the case of SIGWINCH, it is sent to the child process to inform it whenever the terminal size changes. <\/p>\n<p>This can happen, for instance, if you resize your PuTTY window. <\/p>\n<p>Editors like nano and others (e.g. alsamixer, \u2026) which use the full screen and interact with it, need to know the screen size to do their calculations correctly and render the output correctly.<\/p>\n<p>Thus, they listen for this signal, and if it arrives, reset their calculations.<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-5.png\"><img loading=\"lazy\" decoding=\"async\" width=\"684\" height=\"579\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-5.png\" border=\"0\"><\/a><\/p>\n<p>As you can see from this fullscreen example, pexpect sets a smaller screen size than my actual available space in PuTTY \u2013 therefore the output size is limited.<\/p>\n<p>This brings us to:<\/p>\n<h2>Control sequences (ANSI escape codes)<\/h2>\n<p>How is it possible to color output on the command line? <\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-6.png\"><img loading=\"lazy\" decoding=\"async\" width=\"437\" height=\"47\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-6.png\" border=\"0\"><\/a><\/p>\n<p>How is it possible for applications to show interactive progress bars on the command line? (e.g. wget):<\/p>\n<p><a href=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image-7.png\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"184\" title=\"image\" style=\"display: inline; background-image: none;\" alt=\"image\" src=\"https:\/\/pi3g.com\/wp-content\/uploads\/2020\/05\/image_thumb-7.png\" border=\"0\"><\/a><\/p>\n<p>This is done using control sequences, <strong>embedded<\/strong> in the output of the application. (This is an example of in-band information being communicated additionally, much like the phone companies used to signal billing information, etc. in-band as well, allowing phreakers to abuse the system!)<\/p>\n<p>This, for example, will produce a new line: \\r\\n<\/p>\n<p>\\r is for carriage return, it moves the cursor to the beginning of the line without advancing to the next line. <\/p>\n<p>\\n line feed, moves the cursor to the next line.<\/p>\n<p>Yes, even in Linux \u2013 in files usually \\n alone will mean new line and imply a carriage return, but for application output to be interpreted correctly by the <strong>terminal<\/strong>, \\r\\n is the actual output to go to the next line!<\/p>\n<p>This is output by the TTY device driver. <\/p>\n<p><a href=\"https:\/\/pexpect.readthedocs.io\/en\/stable\/overview.html#find-the-end-of-line-cr-lf-conventions\">Read more about this behavior in the pexpect documentation<\/a>.<\/p>\n<p>To create colored text, or move the cursor to any point on the screen, escape sequences can be used.<\/p>\n<p>These escape sequences start with the ESC byte <strong>(27 \/ hex 0x1B \/ oct 033)<\/strong>, and are followed by a second byte in the range 0x40 \u2013 0x5F (ASCII&nbsp; @A-Z[\\]^_ )<\/p>\n<p>one of the most useful sequences is the [ control sequence introducer (or CSI sequences) (ESC is followed by [ in this case). <\/p>\n<p>these sequences can be used to position the cursor, erase part of the screen, set colors, and much more.<\/p>\n<p>Thus, you can set the color of your prompt in the ~\/.bashrc like this:<\/p>\n<blockquote>\n<p>PS1=&#8217;${debian_chroot:+($debian_chroot)}\\t \\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;37m\\]\\w \\$\\[\\033[00m\\] &#8216;<\/p>\n<\/blockquote>\n<p>This used to be \u201cmagic incantations\u201d to me before I understood this topic. you can now recognize the control sequences, which drive your PuTTY interpretation directly<\/p>\n<ul>\n<li>\\[\\033[01;32m\\]<\/li>\n<\/ul>\n<p>In this case \\[ and \\] are related to the bash<\/p>\n<p>The control sequence is inside: \\033 is for ESC (in octal representation), then we have the [, and then 01;32m is the actual sequence setting the foreground color.<\/p>\n<p>Further reading:<\/p>\n<ul>\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/ANSI_escape_code\">Wikipedia on ANSI escape codes<\/a><\/li>\n<\/ul>\n<h2>Actual pexpect code<\/h2>\n<p>Here are some useful snippets of pexpect code (note, pexpect needs to be installed on your system first in the usual way, see pexpect documentation).<\/p>\n<blockquote>\n<p>import pexpect<br \/>\nimport time<\/p>\n<p>child = pexpect.spawn([&#8216;login&#8217;], maxread=1)<\/p>\n<p>time.sleep(0.5)<br \/>\nprint(child.read_nonblocking(size=30, timeout=0))<\/p>\n<p>child.delaybeforesend = None<\/p>\n<p># this sends a right arrow key<br \/>\nchild.send(&#8220;\\033[C&#8221;)<\/p>\n<p>child.interact()<\/p>\n<\/blockquote>\n<p>maxread=1 sets buffering to none. <\/p>\n<p>Important: We would set the delaybeforesend to None, as we will be funneling real user input through pexpect, which has built-in delays by nature; and we do not want to increase latency unnecessary! <\/p>\n<p>For actual non-interactive use (main use case of pexpect), the default is recommended.<\/p>\n<p>interact() will show the output of the application directly, and send your user input to the application. <\/p>\n<p><strong>Please note: for the live example above, child.interact() was run directly after the pexpect.spawn() statement.<\/strong><\/p>\n<h1>Web-frontend<\/h1>\n<p>The plot thickens. What we need on the other side is a JavaScript application capable of understanding and rendering these control sequences. (I have also seen them to be referred to as VT-100 compatible). <\/p>\n<p>My research has led me to <a href=\"https:\/\/xtermjs.org\/\">xterm.js<\/a><\/p>\n<p>It promises performance, compatibility, unicode support, being self-contained, etc.<\/p>\n<p>It is shipped through npm, which is good for my new workflow for picockpit-frontend. <\/p>\n<p>Many applications are based on xterm.js, including Microsoft Visual Studio Code. <\/p>\n<h1>Transport<\/h1>\n<p>Having components on both sides ready, the only thing which is missing is the transport. Here we have to talk about latency, and what users expect.<\/p>\n<p>Since this is an interactive application, we will need to send characters which the user types one by one to the backend \u2013 which should (depending on the active application) immediately echo these characters back.<\/p>\n<p>No buffering is possible here \u2013 the only thing which is possible is that the application will send more data as a reply, or on it\u2019s own, which we can send as a package of data.<\/p>\n<p>But the user input has to be streamed character by character.<\/p>\n<p>My initial thought on this was to send individual, stripped down MQTT messages.<\/p>\n<p>But this conflicts with the user\u2019s privacy.<\/p>\n<p>Even though the data is run through WebSockets (and thus https), the messages pass unencrypted through the picockpit.com MQTT broker (VerneMQ).<\/p>\n<p>Shell interactions will include passwords, and sensitive data \u2013 thus a secure channel MUST be established.<\/p>\n<p>Currently I am thinking of using websockets, and possibly some kind of library along for it, to multiplex several different data streams through one connection each.<\/p>\n<p>The transport bit is the reason that I am delaying TermiShell until the 3.0 release of PiCockpit \u2013 additional tooling has go to into the transport, allowing the transport to be reused for other applications and use cases as well. <\/p>\n<p>Possibly this might be interesting:<\/p>\n<ul>\n<li><a href=\"https:\/\/socket.io\/\">https:\/\/socket.io\/<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Einleitung Im Zuge der Entwicklung von PiCockpit werde ich ein webbasiertes Terminal namens TermiShell hinzuf\u00fcgen. TermiShell Symbol, von: Stephanie Harvey via unsplash.com TermiShell wird es erm\u00f6glichen, sich \u00fcber PiCockpit.com (und den PiCockpit-Client) in den Raspberry Pi einzuloggen - ohne zus\u00e4tzliche Anwendungen auf beiden Seiten. Das sollte sehr komfortabel sein,...<\/p>","protected":false},"author":830,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_links_to":"","_links_to_target":""},"categories":[402,610,1,400],"tags":[781,783,779,782,599,778,572,777,780,602],"class_list":["post-24664","post","type-post","status-publish","format-standard","hentry","category-development","category-python","category-raspberrypi-blog","category-raspberry-pi-embedded-development","tag-ansi-escape-codes","tag-color","tag-console-js","tag-control-sequences","tag-mqtt","tag-pty","tag-python","tag-tty","tag-vt100","tag-websockets"],"_links":{"self":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts\/24664","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/users\/830"}],"replies":[{"embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/comments?post=24664"}],"version-history":[{"count":1,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts\/24664\/revisions"}],"predecessor-version":[{"id":24665,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/posts\/24664\/revisions\/24665"}],"wp:attachment":[{"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/media?parent=24664"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/categories?post=24664"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pi3g.com\/de\/wp-json\/wp\/v2\/tags?post=24664"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}