aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFeuerfuchs <git@feuerfuchs.dev>2019-11-16 00:55:25 +0100
committerFeuerfuchs <git@feuerfuchs.dev>2019-11-16 00:55:25 +0100
commit9bed6157351eea6fdbccff69aba5cc967a0b2a56 (patch)
treee7b555078e7a4593f3e3456121dc8b04ae7b10a1
parentOnly enable max with if wrapping is enabled (diff)
downloadgopherproxy-9bed6157351eea6fdbccff69aba5cc967a0b2a56.tar.gz
gopherproxy-9bed6157351eea6fdbccff69aba5cc967a0b2a56.tar.bz2
gopherproxy-9bed6157351eea6fdbccff69aba5cc967a0b2a56.zip
Initial Gemini support
-rw-r--r--README.md5
-rw-r--r--assets/main.js2
-rw-r--r--assets/style.css2
-rw-r--r--cmd/gopherproxy/main.go2
-rw-r--r--css/main.scss1
-rw-r--r--gopherproxy.go261
-rw-r--r--js/main.ts12
-rw-r--r--libgemini.go153
-rw-r--r--template.go28
9 files changed, 402 insertions, 64 deletions
diff --git a/README.md b/README.md
index f4d5cc6..e4f5d97 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,10 @@
1# Gopher (RFC 1436) Web Proxy 1# Gopher (RFC 1436) Web Proxy
2 2
3gopherproxy is a Gopher (RFC 1436) Web Proxy that acts as a gateway into Gopherspace 3gopherproxy is a Gopher (RFC 1436) and Gemini (gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini/docs/) Web Proxy that acts as a gateway into Gopherspace/Geminispace by proxying standard Web HTTP requests to Gopher/Gemini requests of the target server.
4by proxying standard Web HTTP requests to Gopher requests of the target server.
5 4
6gopherproxy is a fork of [https://github.com/prologic/gopherproxy](https://github.com/prologic/gopherproxy). 5gopherproxy is a fork of [https://github.com/prologic/gopherproxy](https://github.com/prologic/gopherproxy).
7 6
8Demo: https://gopher.vulpes.one/ 7Demo: https://proxy.vulpes.one/
9 8
10 9
11## Requirements 10## Requirements
diff --git a/assets/main.js b/assets/main.js
index e853804..eee70d7 100644
--- a/assets/main.js
+++ b/assets/main.js
@@ -1 +1 @@
"use strict";var KeyValueStore=function(){function e(e){this.data=e;for(var t=0,n=Object.keys(e);t<n.length;t++){var a=n[t],l=e[a];if(l.valueRange&&-1===l.valueRange.indexOf(l.value))throw new Error('Invalid value "'+l.value+'" for ID "'+a+'"')}}return e.prototype.getValue=function(e){return this.data[e].value},e.prototype.setValue=function(e,t){var n=this.data[e];if(n.valueRange&&-1===n.valueRange.indexOf(t))throw new Error('Invalid value "'+t+'" for ID "'+e+'"');n.value=t,n.callbacks&&n.callbacks.forEach((function(e){e(t)}))},e.prototype.cycleValue=function(e,t){void 0===t&&(t=1);var n=this.data[e];if(!n)throw new Error('Invalid ID "'+e+'"');var a=n.value;if(n.valueRange){var l=n.valueRange.indexOf(a)+t;l>=n.valueRange.length?l=0:l<0&&(l=n.valueRange.length-1),a=n.value=n.valueRange[l]}else{if("number"!=typeof a)throw new Error("Can't cycle \""+e+'"');a+=t,n.value=a}return n.callbacks&&n.callbacks.forEach((function(e){e(a)})),a},e.prototype.addCallback=function(e,t){var n=this.data[e];n.callbacks||(n.callbacks=[]),n.callbacks.push(t)},e}();function ensureSetting(e,t){var n=localStorage.getItem(e);return null===n&&(n=t,localStorage.setItem(e,n)),n}var settings=new KeyValueStore({wordWrap:{value:"1"===ensureSetting("word-wrap","1"),callbacks:[function(e){localStorage.setItem("word-wrap",e?"1":"0")}],valueRange:[!1,!0]},monospaceFont:{value:"1"===ensureSetting("monospace-font","1"),callbacks:[function(e){localStorage.setItem("monospace-font",e?"1":"0")}],valueRange:[!1,!0]},imagePreviews:{value:"1"===ensureSetting("image-previews","1"),callbacks:[function(e){localStorage.setItem("image-previews",e?"1":"0")}],valueRange:[!1,!0]},clickablePlainLinks:{value:"1"===ensureSetting("clickable-plain-links","1"),callbacks:[function(e){localStorage.setItem("clickable-plain-links",e?"1":"0")}],valueRange:[!1,!0]}});function generateImageThumbnails(){for(var e=document.querySelectorAll(".link--IMG, .link--GIF"),t=e.length,n=function(){var n=e[t],a=n.href.replace(/^(.*?)\/I/,"$1/T"),l=document.createTextNode("\n"),s=document.createElement("span");s.classList.add("type-annotation"),s.textContent=" -> ";var i=document.createElement("img");i.src=a,i.addEventListener("load",(function(e){i.classList.remove("faded")}));var r=document.createElement("a");r.classList.add("img-preview"),r.href=n.href,r.addEventListener("click",(function(e){return e.preventDefault(),i.classList.add("faded"),i.classList.contains("expanded")?(i.classList.remove("expanded"),i.src=a):(i.classList.add("expanded"),i.src=r.href),!1})),r.append(i),n.parentNode.insertBefore(r,n.nextSibling),n.parentNode.insertBefore(s,r),n.parentNode.insertBefore(l,s)};t--;)n()}function removeImageThumbnails(){for(var e=document.querySelectorAll(".link--IMG, .link--GIF"),t=e.length;t--;)for(var n=e[t],a=3;a--&&n.nextSibling;)n.nextSibling.remove()}function generateMarkupForPlainLinks(){if(document.body.classList.contains("is-plain")){var e=document.getElementsByClassName("content")[0];e.innerHTML=e.innerHTML.replace(/\b[a-z]*:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,}\b[-a-zA-Z0-9@:%_\+.,~#?&//=]*/g,(function(e){var t=e;return 0===t.indexOf("gopher://")&&(t=t.replace(/^gopher:\/\/(.*)$/,location.origin+"/$1")),'<a href="'+t+'">'+e+"</a>"})),e.innerHTML=e.innerHTML.replace(/\bmailto:[-a-zA-Z0-9@:%._\+~#=]+@(?:[-a-zA-Z0-9@:%._\+~#=]+\.)+[a-z]{2,}\b/g,(function(e){return'<a href="'+e+'">'+e+"</a>"}))}}function removeMarkupForPlainLinks(){if(document.body.classList.contains("is-plain"))for(var e=document.getElementsByClassName("content")[0],t=e.getElementsByTagName("a"),n=t.length;n--;){var a=t[n],l=document.createTextNode(a.textContent);e.replaceChild(l,a)}}!function(){for(var e=document.getElementsByClassName("link--QRY"),t=e.length;t--;)e[t].addEventListener("click",(function(e){e.preventDefault();var t=prompt("Please enter required input: ","");return null!==t&&""!==t&&(window.location.href=e.target.href+"?"+t),!1}))}(),function(){for(var e=document.getElementsByClassName("location__prefix"),t=e.length;t--;)e[t].addEventListener("click",(function(e){e.preventDefault();var t=prompt("Please enter new location: ","");return null!==t&&""!==t.trim()&&(0===(t=t.trim()).indexOf("gopher://")&&(t=t.substring(9)),window.location.href=window.location.origin+"/"+t),!1}))}(),function(){var e=document.getElementsByClassName("wrap")[0],t=e.getElementsByClassName("content")[0],n=document.getElementsByClassName("setting--image-previews")[0].getElementsByClassName("setting__value")[0],a=function(e,t){void 0===t&&(t=!1),e?generateImageThumbnails():t||removeImageThumbnails(),n.textContent=e?"[yes]":"[no]"};n.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("imagePreviews"),!1})),a(settings.getValue("imagePreviews"),!0),settings.addCallback("imagePreviews",a);var l=document.getElementsByClassName("setting--monospace-font")[0].getElementsByClassName("setting__value")[0],s=function(e){e?t.classList.add("content--has-monospace-font"):t.classList.remove("content--has-monospace-font"),l.textContent=e?"[yes]":"[no]"};l.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("monospaceFont"),!1})),s(settings.getValue("monospaceFont")),settings.addCallback("monospaceFont",s);var i=document.getElementsByClassName("setting--word-wrap")[0].getElementsByClassName("setting__value")[0],r=function(t){t?e.classList.add("wrap--word-wrap"):e.classList.remove("wrap--word-wrap"),i.textContent=t?"[yes]":"[no]"};i.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("wordWrap"),!1})),r(settings.getValue("wordWrap")),settings.addCallback("wordWrap",r);var o=document.getElementsByClassName("setting--clickable-plain-links")[0].getElementsByClassName("setting__value")[0],c=function(e){e?generateMarkupForPlainLinks():removeMarkupForPlainLinks(),o.textContent=e?"[yes]":"[no]"};o.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("clickablePlainLinks"),!1})),c(settings.getValue("clickablePlainLinks")),settings.addCallback("clickablePlainLinks",c)}(),function(){for(var e=document.getElementsByClassName("modal"),t=e.length,n=function(){var n=e[t],a=n.getElementsByClassName("modal__content")[0],l=n.getElementsByClassName("modal__close-btn")[0];document.addEventListener("click",(function(e){n.classList.contains("modal--visible")&&(e.target===a||a.contains(e.target)||(n.classList.remove("modal--visible"),e.preventDefault(),e.stopPropagation()))}),!0),document.addEventListener("keydown",(function(e){n.classList.contains("modal--visible")&&27===e.keyCode&&n.classList.remove("modal--visible")})),l.addEventListener("click",(function(e){return e.preventDefault(),n.classList.remove("modal--visible"),!1}))};t--;)n();var a=document.getElementsByClassName("settings-btn")[0],l=document.getElementsByClassName("modal--settings")[0];a.addEventListener("click",(function(e){return e.preventDefault(),l.classList.add("modal--visible"),!1}))}(); \ No newline at end of file "use strict";var KeyValueStore=function(){function e(e){this.data=e;for(var t=0,n=Object.keys(e);t<n.length;t++){var a=n[t],l=e[a];if(l.valueRange&&-1===l.valueRange.indexOf(l.value))throw new Error('Invalid value "'+l.value+'" for ID "'+a+'"')}}return e.prototype.getValue=function(e){return this.data[e].value},e.prototype.setValue=function(e,t){var n=this.data[e];if(n.valueRange&&-1===n.valueRange.indexOf(t))throw new Error('Invalid value "'+t+'" for ID "'+e+'"');n.value=t,n.callbacks&&n.callbacks.forEach((function(e){e(t)}))},e.prototype.cycleValue=function(e,t){void 0===t&&(t=1);var n=this.data[e];if(!n)throw new Error('Invalid ID "'+e+'"');var a=n.value;if(n.valueRange){var l=n.valueRange.indexOf(a)+t;l>=n.valueRange.length?l=0:l<0&&(l=n.valueRange.length-1),a=n.value=n.valueRange[l]}else{if("number"!=typeof a)throw new Error("Can't cycle \""+e+'"');a+=t,n.value=a}return n.callbacks&&n.callbacks.forEach((function(e){e(a)})),a},e.prototype.addCallback=function(e,t){var n=this.data[e];n.callbacks||(n.callbacks=[]),n.callbacks.push(t)},e}();function ensureSetting(e,t){var n=localStorage.getItem(e);return null===n&&(n=t,localStorage.setItem(e,n)),n}var settings=new KeyValueStore({wordWrap:{value:"1"===ensureSetting("word-wrap","1"),callbacks:[function(e){localStorage.setItem("word-wrap",e?"1":"0")}],valueRange:[!1,!0]},monospaceFont:{value:"1"===ensureSetting("monospace-font","1"),callbacks:[function(e){localStorage.setItem("monospace-font",e?"1":"0")}],valueRange:[!1,!0]},imagePreviews:{value:"1"===ensureSetting("image-previews","1"),callbacks:[function(e){localStorage.setItem("image-previews",e?"1":"0")}],valueRange:[!1,!0]},clickablePlainLinks:{value:"1"===ensureSetting("clickable-plain-links","1"),callbacks:[function(e){localStorage.setItem("clickable-plain-links",e?"1":"0")}],valueRange:[!1,!0]}});function generateImageThumbnails(){for(var e=document.querySelectorAll(".link--IMG, .link--GIF"),t=e.length,n=function(){var n=e[t],a=n.href.replace(/^(.*?)\/I/,"$1/T"),l=document.createTextNode("\n"),i=document.createElement("span");i.classList.add("type-annotation"),i.textContent=" -> ";var s=document.createElement("img");s.src=a,s.addEventListener("load",(function(e){s.classList.remove("faded")}));var r=document.createElement("a");r.classList.add("img-preview"),r.href=n.href,r.addEventListener("click",(function(e){return e.preventDefault(),s.classList.add("faded"),s.classList.contains("expanded")?(s.classList.remove("expanded"),s.src=a):(s.classList.add("expanded"),s.src=r.href),!1})),r.append(s),n.parentNode.insertBefore(r,n.nextSibling),n.parentNode.insertBefore(i,r),n.parentNode.insertBefore(l,i)};t--;)n()}function removeImageThumbnails(){for(var e=document.querySelectorAll(".link--IMG, .link--GIF"),t=e.length;t--;)for(var n=e[t],a=3;a--&&n.nextSibling;)n.nextSibling.remove()}function generateMarkupForPlainLinks(){if(document.body.classList.contains("is-plain")){var e=document.getElementsByClassName("content")[0];e.innerHTML=e.innerHTML.replace(/\b[a-z]*:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,}\b[-a-zA-Z0-9@:%_\+.,~#?&//=]*/g,(function(e){var t=e;return 0===t.indexOf("gopher://")?t=t.replace(/^gopher:\/\/(.*)$/,location.origin+"/gopher/$1"):0===t.indexOf("gemini://")&&(t=t.replace(/^gemini:\/\/(.*)$/,location.origin+"/gemini/$1")),'<a href="'+t+'">'+e+"</a>"})),e.innerHTML=e.innerHTML.replace(/\bmailto:[-a-zA-Z0-9@:%._\+~#=]+@(?:[-a-zA-Z0-9@:%._\+~#=]+\.)+[a-z]{2,}\b/g,(function(e){return'<a href="'+e+'">'+e+"</a>"}))}}function removeMarkupForPlainLinks(){if(document.body.classList.contains("is-plain"))for(var e=document.getElementsByClassName("content")[0],t=e.getElementsByTagName("a"),n=t.length;n--;){var a=t[n],l=document.createTextNode(a.textContent);e.replaceChild(l,a)}}!function(){for(var e=document.getElementsByClassName("link--QRY"),t=e.length;t--;)e[t].addEventListener("click",(function(e){e.preventDefault();var t=prompt("Please enter required input: ","");return null!==t&&""!==t&&(window.location.href=e.target.href+"?"+t),!1}))}(),function(){for(var e=document.getElementsByClassName("location__prefix"),t=e.length;t--;)e[t].addEventListener("click",(function(e){e.preventDefault();var t=prompt("Please enter new location (gopher://... or gemini://...):","");return null!==t&&""!==t.trim()&&(t=0===(t=t.trim()).indexOf("gopher://")?"gopher/"+t.substring(9):0===t.indexOf("gemini://")?"gemini/"+t.substring(9):"gopher/"+t,window.location.href=window.location.origin+"/"+t),!1}))}(),function(){var e=document.getElementsByClassName("wrap")[0],t=e.getElementsByClassName("content")[0],n=document.getElementsByClassName("setting--image-previews")[0].getElementsByClassName("setting__value")[0],a=function(e,t){void 0===t&&(t=!1),e?generateImageThumbnails():t||removeImageThumbnails(),n.textContent=e?"[yes]":"[no]"};n.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("imagePreviews"),!1})),a(settings.getValue("imagePreviews"),!0),settings.addCallback("imagePreviews",a);var l=document.getElementsByClassName("setting--monospace-font")[0].getElementsByClassName("setting__value")[0],i=function(e){e?t.classList.add("content--has-monospace-font"):t.classList.remove("content--has-monospace-font"),l.textContent=e?"[yes]":"[no]"};l.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("monospaceFont"),!1})),i(settings.getValue("monospaceFont")),settings.addCallback("monospaceFont",i);var s=document.getElementsByClassName("setting--word-wrap")[0].getElementsByClassName("setting__value")[0],r=function(t){t?e.classList.add("wrap--word-wrap"):e.classList.remove("wrap--word-wrap"),s.textContent=t?"[yes]":"[no]"};s.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("wordWrap"),!1})),r(settings.getValue("wordWrap")),settings.addCallback("wordWrap",r);var o=document.getElementsByClassName("setting--clickable-plain-links")[0].getElementsByClassName("setting__value")[0],c=function(e){e?generateMarkupForPlainLinks():removeMarkupForPlainLinks(),o.textContent=e?"[yes]":"[no]"};o.addEventListener("click",(function(e){return e.preventDefault(),settings.cycleValue("clickablePlainLinks"),!1})),c(settings.getValue("clickablePlainLinks")),settings.addCallback("clickablePlainLinks",c)}(),function(){for(var e=document.getElementsByClassName("modal"),t=e.length,n=function(){var n=e[t],a=n.getElementsByClassName("modal__content")[0],l=n.getElementsByClassName("modal__close-btn")[0];document.addEventListener("click",(function(e){n.classList.contains("modal--visible")&&(e.target===a||a.contains(e.target)||(n.classList.remove("modal--visible"),e.preventDefault(),e.stopPropagation()))}),!0),document.addEventListener("keydown",(function(e){n.classList.contains("modal--visible")&&27===e.keyCode&&n.classList.remove("modal--visible")})),l.addEventListener("click",(function(e){return e.preventDefault(),n.classList.remove("modal--visible"),!1}))};t--;)n();var a=document.getElementsByClassName("settings-btn")[0],l=document.getElementsByClassName("modal--settings")[0];a.addEventListener("click",(function(e){return e.preventDefault(),l.classList.add("modal--visible"),!1}))}(); \ No newline at end of file
diff --git a/assets/style.css b/assets/style.css
index c52c2ad..70f53a7 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -1 +1 @@
body{margin:0;padding:0;background-color:#14171a;color:#cad1d8;font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace;font-size:1.0625em;line-height:1.5}h1,h2,h3,h4,h5,h6{font:inherit;color:#fff;margin:0}button{background:none;border:0;padding:0;color:#fff;font:inherit;text-decoration:underline;cursor:pointer}button:focus{outline:1px dotted currentColor}img{display:inline-block;vertical-align:top;max-width:8em;margin:.1em 0}img::selection{background-color:rgba(239,198,138,0.35)}img.expanded{max-width:40em;max-width:80ch}img.faded{opacity:.5}strong{font-weight:normal}::selection{color:#000;background-color:rgba(239,198,138,0.996)}:link{color:#fff}:visited{color:#cad1d8}:link:hover,:visited:hover{color:#fff}.header-base{display:flex;flex-direction:row;align-items:center;justify-content:space-between;border-bottom:1px solid #353a3f}.header{padding:.9em 1em;color:#929ba3}.location{flex:0 1 auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-overflow:ellipsis '|';margin-right:.5em}.location__prefix{margin-right:.3em;color:#929ba3}@media (hover: hover){.location__prefix:hover{color:#fff}}.location__prefix--mobile{display:none}.location__slash{margin:0 .3em}.location__uripart{color:#fff}.location__uripart+.location__slash+.location__uripart{color:#cad1d8}.location__uripart:link:hover,.location__uripart:visited:hover{color:#fff}.actions{flex:0 0 auto}.actions :visited{color:#fff}.action{display:inline}.action+.action::before{content:' | '}.wrap{padding:2em 1em;text-align:center}.wrap--word-wrap{max-width:50em;max-width:100ch;margin:0 auto}.wrap--word-wrap .content{white-space:pre-wrap;word-wrap:break-word}.content{box-sizing:border-box;display:inline-block;min-width:0;max-width:100%;margin:0;padding:0;text-align:left;font:inherit;font-family:"Iosevka Aile","Fira Sans","Roboto","Droid Sans",sans-serif}.content--has-type-annotations{padding-left:3em;padding-left:5ch}.content--has-monospace-font{font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace}.type-annotation{margin-left:-3em;margin-left:-5ch;color:#929ba3;white-space:pre;font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace}.modal{position:fixed;top:0;left:0;z-index:100;display:none;width:100%;height:100%;box-sizing:border-box;padding:2em;background-color:rgba(0,0,0,0.75)}.modal--visible{display:block}.modal__content{max-width:30em;padding:1.5em 1.8em;margin:0 auto;background-color:#14171a;box-shadow:0 .3em 2em #000;text-align:left}.modal__head{padding-bottom:.75em;margin-bottom:1.5em}.modal__title{padding-right:1em;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;text-transform:uppercase}.setting{display:flex;flex-direction:row;align-items:baseline;justify-content:space-between}.setting::after{order:2;flex:1 1 auto;display:block;height:0;margin:0 .5em;border-bottom:2px dotted #353a3f;content:''}.setting__label{order:1}.setting__value{order:3}@media screen and (max-width: 800px){body{font-size:1em}.modal{padding:1em}.modal__content{padding:1em 1.3em}}@media screen and (max-width: 500px){.location__prefix{display:none}.location__prefix--mobile{display:inline}.action{display:block}.action+.action::before{content:''}}@media screen and (max-width: 280px){.location__prefix--mobile{display:none}} body{margin:0;padding:0;background-color:#14171a;color:#cad1d8;font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace;font-size:1.0625em;line-height:1.5}h1,h2,h3,h4,h5,h6{font:inherit;color:#fff;margin:0}button{background:none;border:0;padding:0;color:#fff;font:inherit;text-decoration:underline;cursor:pointer}button:focus{outline:1px dotted currentColor}img{display:inline-block;vertical-align:top;max-width:8em;margin:.1em 0}img::selection{background-color:rgba(239,198,138,0.35)}img.expanded{max-width:40em;max-width:80ch}img.faded{opacity:.5}strong{font-weight:normal}::selection{color:#000;background-color:rgba(239,198,138,0.996)}:link{color:#fff}:visited{color:#cad1d8}:link:hover,:visited:hover{color:#fff}.header-base{display:flex;flex-direction:row;align-items:center;justify-content:space-between;border-bottom:1px solid #353a3f}.header{padding:.9em 1em;color:#929ba3}.location{flex:0 1 auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-overflow:ellipsis '|';margin-right:.5em}.location__prefix{margin-right:.3em;color:#929ba3;cursor:pointer}@media (hover: hover){.location__prefix:hover{color:#fff}}.location__prefix--mobile{display:none}.location__slash{margin:0 .3em}.location__uripart{color:#fff}.location__uripart+.location__slash+.location__uripart{color:#cad1d8}.location__uripart:link:hover,.location__uripart:visited:hover{color:#fff}.actions{flex:0 0 auto}.actions :visited{color:#fff}.action{display:inline}.action+.action::before{content:' | '}.wrap{padding:2em 1em;text-align:center}.wrap--word-wrap{max-width:50em;max-width:100ch;margin:0 auto}.wrap--word-wrap .content{white-space:pre-wrap;word-wrap:break-word}.content{box-sizing:border-box;display:inline-block;min-width:0;max-width:100%;margin:0;padding:0;text-align:left;font:inherit;font-family:"Iosevka Aile","Fira Sans","Roboto","Droid Sans",sans-serif}.content--has-type-annotations{padding-left:3em;padding-left:5ch}.content--has-monospace-font{font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace}.type-annotation{margin-left:-3em;margin-left:-5ch;color:#929ba3;white-space:pre;font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace}.modal{position:fixed;top:0;left:0;z-index:100;display:none;width:100%;height:100%;box-sizing:border-box;padding:2em;background-color:rgba(0,0,0,0.75)}.modal--visible{display:block}.modal__content{max-width:30em;padding:1.5em 1.8em;margin:0 auto;background-color:#14171a;box-shadow:0 .3em 2em #000;text-align:left}.modal__head{padding-bottom:.75em;margin-bottom:1.5em}.modal__title{padding-right:1em;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;text-transform:uppercase}.setting{display:flex;flex-direction:row;align-items:baseline;justify-content:space-between}.setting::after{order:2;flex:1 1 auto;display:block;height:0;margin:0 .5em;border-bottom:2px dotted #353a3f;content:''}.setting__label{order:1}.setting__value{order:3}@media screen and (max-width: 800px){body{font-size:1em}.modal{padding:1em}.modal__content{padding:1em 1.3em}}@media screen and (max-width: 500px){.location__prefix{display:none}.location__prefix--mobile{display:inline}.action{display:block}.action+.action::before{content:''}}@media screen and (max-width: 280px){.location__prefix--mobile{display:none}}
diff --git a/cmd/gopherproxy/main.go b/cmd/gopherproxy/main.go
index e699d14..66248ac 100644
--- a/cmd/gopherproxy/main.go
+++ b/cmd/gopherproxy/main.go
@@ -13,7 +13,7 @@ var (
13 bind = flag.String("bind", "0.0.0.0:8000", "[int]:port to bind to") 13 bind = flag.String("bind", "0.0.0.0:8000", "[int]:port to bind to")
14 robotsfile = flag.String("robots-file", "robots.txt", "robots.txt file") 14 robotsfile = flag.String("robots-file", "robots.txt", "robots.txt file")
15 robotsdebug = flag.Bool("robots-debug", false, "print output about ignored robots.txt") 15 robotsdebug = flag.Bool("robots-debug", false, "print output about ignored robots.txt")
16 uri = flag.String("uri", "floodgap.com", "<host>:[port] to proxy to") 16 uri = flag.String("uri", "gopher/floodgap.com", "<gopher|gemini>/<host>:[port] to proxy to")
17 vipsconcurrency = flag.Int("vips-concurrency", 1, "Concurrency level of libvips") 17 vipsconcurrency = flag.Int("vips-concurrency", 1, "Concurrency level of libvips")
18) 18)
19 19
diff --git a/css/main.scss b/css/main.scss
index bed3b24..42b0b28 100644
--- a/css/main.scss
+++ b/css/main.scss
@@ -129,6 +129,7 @@ strong {
129 &__prefix { 129 &__prefix {
130 margin-right: .3em; 130 margin-right: .3em;
131 color: $text-minus; 131 color: $text-minus;
132 cursor: pointer;
132 133
133 @media (hover: hover) { 134 @media (hover: hover) {
134 &:hover { 135 &:hover {
diff --git a/gopherproxy.go b/gopherproxy.go
index 87a1ad0..9a60507 100644
--- a/gopherproxy.go
+++ b/gopherproxy.go
@@ -1,6 +1,7 @@
1package gopherproxy 1package gopherproxy
2 2
3import ( 3import (
4 "bufio"
4 "bytes" 5 "bytes"
5 "crypto/md5" 6 "crypto/md5"
6 "fmt" 7 "fmt"
@@ -25,6 +26,11 @@ import (
25 "github.com/NYTimes/gziphandler" 26 "github.com/NYTimes/gziphandler"
26) 27)
27 28
29const (
30 ITEM_TYPE_GEMINI_LINE = ""
31 ITEM_TYPE_GEMINI_LINK = " =>"
32)
33
28type Item struct { 34type Item struct {
29 Link template.URL 35 Link template.URL
30 Type string 36 Type string
@@ -40,7 +46,7 @@ type AssetList struct {
40 PropFontW2 string 46 PropFontW2 string
41} 47}
42 48
43func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { 49func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error {
44 var title string 50 var title string
45 51
46 out := make([]Item, len(d.Items)) 52 out := make([]Item, len(d.Items))
@@ -76,7 +82,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As
76 path = strings.Replace(path, "%2F", "/", -1) 82 path = strings.Replace(path, "%2F", "/", -1)
77 tr.Link = template.URL( 83 tr.Link = template.URL(
78 fmt.Sprintf( 84 fmt.Sprintf(
79 "/%s/%s%s", 85 "/gopher/%s/%s%s",
80 hostport, 86 hostport,
81 string(byte(x.Type)), 87 string(byte(x.Type)),
82 path, 88 path,
@@ -92,13 +98,81 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As
92 } 98 }
93 99
94 return tpl.Execute(w, struct { 100 return tpl.Execute(w, struct {
95 Title string 101 Title string
96 URI string 102 URI string
97 Assets AssetList 103 Assets AssetList
98 Lines []Item 104 Lines []Item
99 RawText string 105 RawText string
100 Error bool 106 Error bool
101 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false}) 107 Protocol string
108 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false, "gopher"})
109}
110
111func parseGeminiDocument(response *GeminiResponse, uri string, hostport string) (items []Item) {
112 scanner := bufio.NewScanner(response.Body)
113 scanner.Split(bufio.ScanLines)
114
115 baseUrl, err := url.Parse(fmt.Sprintf(
116 "gemini://%s/%s",
117 hostport,
118 uri,
119 ))
120 if err != nil {
121 return []Item{}
122 }
123
124 for scanner.Scan() {
125 line := strings.Trim(scanner.Text(), "\r\n")
126
127 item := Item{
128 Type: ITEM_TYPE_GEMINI_LINE,
129 Text: line,
130 }
131
132 linkMatch := GeminiLinkPattern.FindStringSubmatch(line)
133 if len(linkMatch) != 0 && linkMatch[0] != "" {
134 link := linkMatch[1]
135
136 if strings.HasPrefix(link, "//") {
137 link = "/gemini/" + strings.TrimPrefix(link, "//")
138 } else if strings.HasPrefix(link, "gemini://") {
139 link = "/gemini/" + strings.TrimPrefix(link, "gemini://")
140 } else if strings.HasPrefix(link, "gopher://") {
141 link = "/gopher/" + strings.TrimPrefix(link, "gopher://")
142 } else {
143 linkUrl, err := url.Parse(link)
144 if err != nil {
145 continue
146 }
147 adjustedUrl := baseUrl.ResolveReference(linkUrl)
148 if adjustedUrl.Scheme == "gemini" {
149 link = "/gemini/" + adjustedUrl.Host + adjustedUrl.Path
150 } else if adjustedUrl.Scheme == "gopher" {
151 link = "/gopher/" + adjustedUrl.Host + adjustedUrl.Path
152 } else {
153 link = adjustedUrl.String()
154 }
155 }
156
157 item.Type = ITEM_TYPE_GEMINI_LINK
158 item.Link = template.URL(link)
159 if linkMatch[2] != "" {
160 item.Text = linkMatch[2]
161 } else {
162 item.Text = linkMatch[1]
163 }
164 }
165
166 items = append(items, item)
167 }
168
169 return
170}
171
172func DefaultHandler(tpl *template.Template, uri string) http.HandlerFunc {
173 return func(w http.ResponseWriter, req *http.Request) {
174 http.Redirect(w, req, "/"+uri, http.StatusFound)
175 }
102} 176}
103 177
104// GopherHandler returns a Handler that proxies requests 178// GopherHandler returns a Handler that proxies requests
@@ -109,7 +183,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As
109func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { 183func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc {
110 return func(w http.ResponseWriter, req *http.Request) { 184 return func(w http.ResponseWriter, req *http.Request) {
111 agent := req.UserAgent() 185 agent := req.UserAgent()
112 path := strings.TrimPrefix(req.URL.Path, "/") 186 path := strings.TrimPrefix(req.URL.Path, "/gopher/")
113 187
114 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { 188 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) {
115 log.Printf("UserAgent %s ignored robots.txt", agent) 189 log.Printf("UserAgent %s ignored robots.txt", agent)
@@ -132,13 +206,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass
132 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) 206 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/"))
133 if err != nil { 207 if err != nil {
134 tpl.Execute(w, struct { 208 tpl.Execute(w, struct {
135 Title string 209 Title string
136 URI string 210 URI string
137 Assets AssetList 211 Assets AssetList
138 RawText string 212 RawText string
139 Lines []Item 213 Lines []Item
140 Error bool 214 Error bool
141 }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true}) 215 Protocol string
216 }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"})
142 return 217 return
143 } 218 }
144 219
@@ -153,13 +228,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass
153 228
154 if err != nil { 229 if err != nil {
155 tpl.Execute(w, struct { 230 tpl.Execute(w, struct {
156 Title string 231 Title string
157 URI string 232 URI string
158 Assets AssetList 233 Assets AssetList
159 RawText string 234 RawText string
160 Lines []Item 235 Lines []Item
161 Error bool 236 Error bool
162 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) 237 Protocol string
238 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"})
163 return 239 return
164 } 240 }
165 241
@@ -171,13 +247,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass
171 buf := new(bytes.Buffer) 247 buf := new(bytes.Buffer)
172 buf.ReadFrom(res.Body) 248 buf.ReadFrom(res.Body)
173 tpl.Execute(w, struct { 249 tpl.Execute(w, struct {
174 Title string 250 Title string
175 URI string 251 URI string
176 Assets AssetList 252 Assets AssetList
177 RawText string 253 RawText string
178 Lines []Item 254 Lines []Item
179 Error bool 255 Error bool
180 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false}) 256 Protocol string
257 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"})
181 } else if strings.HasPrefix(parts[1], "T") { 258 } else if strings.HasPrefix(parts[1], "T") {
182 _, _, err = vips.NewTransform(). 259 _, _, err = vips.NewTransform().
183 Load(res.Body). 260 Load(res.Body).
@@ -190,21 +267,123 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass
190 io.Copy(w, res.Body) 267 io.Copy(w, res.Body)
191 } 268 }
192 } else { 269 } else {
193 if err := renderDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { 270 if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil {
194 tpl.Execute(w, struct { 271 tpl.Execute(w, struct {
195 Title string 272 Title string
196 URI string 273 URI string
197 Assets AssetList 274 Assets AssetList
198 RawText string 275 RawText string
199 Lines []Item 276 Lines []Item
200 Error bool 277 Error bool
201 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) 278 Protocol string
279 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"})
202 return 280 return
203 } 281 }
204 } 282 }
205 } 283 }
206} 284}
207 285
286func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc {
287 return func(w http.ResponseWriter, req *http.Request) {
288 agent := req.UserAgent()
289 path := strings.TrimPrefix(req.URL.Path, "/gemini/")
290
291 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) {
292 log.Printf("UserAgent %s ignored robots.txt", agent)
293 }
294
295 parts := strings.Split(path, "/")
296 hostport := parts[0]
297
298 if len(hostport) == 0 {
299 http.Redirect(w, req, "/"+uri, http.StatusFound)
300 return
301 }
302
303 var qs string
304
305 if req.URL.RawQuery != "" {
306 qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery))
307 }
308
309 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/"))
310 if err != nil {
311 tpl.Execute(w, struct {
312 Title string
313 URI string
314 Assets AssetList
315 RawText string
316 Lines []Item
317 Error bool
318 Protocol string
319 }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"})
320 return
321 }
322
323 res, err := GeminiGet(
324 fmt.Sprintf(
325 "gemini://%s/%s%s",
326 hostport,
327 uri,
328 qs,
329 ),
330 )
331
332 if err != nil {
333 tpl.Execute(w, struct {
334 Title string
335 URI string
336 Assets AssetList
337 RawText string
338 Lines []Item
339 Error bool
340 Protocol string
341 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"})
342 return
343 }
344
345 if int(res.Header.Status/10) != 2 {
346 tpl.Execute(w, struct {
347 Title string
348 URI string
349 Assets AssetList
350 RawText string
351 Lines []Item
352 Error bool
353 Protocol string
354 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"})
355 return
356 }
357
358 if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) {
359 items := parseGeminiDocument(res, uri, hostport)
360 tpl.Execute(w, struct {
361 Title string
362 URI string
363 Assets AssetList
364 RawText string
365 Lines []Item
366 Error bool
367 Protocol string
368 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, "", items, false, "gemini"})
369 } else if strings.HasPrefix(res.Header.Meta, "text/") {
370 buf := new(bytes.Buffer)
371 buf.ReadFrom(res.Body)
372 tpl.Execute(w, struct {
373 Title string
374 URI string
375 Assets AssetList
376 RawText string
377 Lines []Item
378 Error bool
379 Protocol string
380 }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gemini"})
381 } else {
382 io.Copy(w, res.Body)
383 }
384 }
385}
386
208// RobotsTxtHandler returns the contents of the robots.txt file 387// RobotsTxtHandler returns the contents of the robots.txt file
209// if configured and valid. 388// if configured and valid.
210func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { 389func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc {
@@ -386,7 +565,9 @@ func ListenAndServe(bind, robotsfile string, robotsdebug bool, vipsconcurrency i
386 ConcurrencyLevel: vipsconcurrency, 565 ConcurrencyLevel: vipsconcurrency,
387 }) 566 })
388 567
389 http.Handle("/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) 568 http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, uri)))
569 http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri)))
570 http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri)))
390 http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) 571 http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata)))
391 http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) 572 http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata)))
392 http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) 573 http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata)))
diff --git a/js/main.ts b/js/main.ts
index 7e0c8a1..2d11ea4 100644
--- a/js/main.ts
+++ b/js/main.ts
@@ -76,11 +76,15 @@ const settings = new KeyValueStore({
76 locationPrefixEls[i].addEventListener('click', e => { 76 locationPrefixEls[i].addEventListener('click', e => {
77 e.preventDefault(); 77 e.preventDefault();
78 78
79 let resp = prompt('Please enter new location: ', ''); 79 let resp = prompt('Please enter new location (gopher://... or gemini://...):', '');
80 if ((resp !== null) && (resp.trim() !== "")) { 80 if ((resp !== null) && (resp.trim() !== "")) {
81 resp = resp.trim(); 81 resp = resp.trim();
82 if (resp.indexOf('gopher://') === 0) { 82 if (resp.indexOf('gopher://') === 0) {
83 resp = resp.substring(9); 83 resp = 'gopher/' + resp.substring(9);
84 } else if (resp.indexOf('gemini://') === 0) {
85 resp = 'gemini/' + resp.substring(9);
86 } else {
87 resp = 'gopher/' + resp;
84 } 88 }
85 89
86 window.location.href = window.location.origin + '/' + resp; 90 window.location.href = window.location.origin + '/' + resp;
@@ -305,7 +309,9 @@ function generateMarkupForPlainLinks() {
305 contentEl.innerHTML = contentEl.innerHTML.replace(urlRegex, match => { 309 contentEl.innerHTML = contentEl.innerHTML.replace(urlRegex, match => {
306 let href: string = match; 310 let href: string = match;
307 if (href.indexOf('gopher://') === 0) { 311 if (href.indexOf('gopher://') === 0) {
308 href = href.replace(/^gopher:\/\/(.*)$/, location.origin + '/$1'); 312 href = href.replace(/^gopher:\/\/(.*)$/, location.origin + '/gopher/$1');
313 } else if (href.indexOf('gemini://') === 0) {
314 href = href.replace(/^gemini:\/\/(.*)$/, location.origin + '/gemini/$1');
309 } 315 }
310 return `<a href="${href}">${match}</a>`; 316 return `<a href="${href}">${match}</a>`;
311 }); 317 });
diff --git a/libgemini.go b/libgemini.go
new file mode 100644
index 0000000..05321ef
--- /dev/null
+++ b/libgemini.go
@@ -0,0 +1,153 @@
1package gopherproxy
2
3import (
4 "bufio"
5 "crypto/tls"
6 "errors"
7 "io"
8 "net/url"
9 "regexp"
10 "strconv"
11 "strings"
12)
13
14const (
15 CRLF = "\r\n"
16)
17
18const (
19 STATUS_INPUT = 10
20 STATUS_SUCCESS = 20
21 STATUS_SUCCESS_CERT = 21
22 STATUS_REDIRECT_TEMP = 30
23 STATUS_REDIRECT_PERM = 31
24 STATUS_TEMP_FAILURE = 40
25 STATUS_SERVER_UNAVAILABLE = 41
26 STATUS_CGI_ERROR = 42
27 STATUS_PROXY_ERROR = 43
28 STATUS_SLOW_DOWN = 44
29 STATUS_PERM_FAILURE = 50
30 STATUS_NOT_FOUND = 51
31 STATUS_GONE = 52
32 STATUS_PROXY_REFUSED = 53
33 STATUS_BAD_REQUEST = 59
34 STATUS_CLIENT_CERT_EXPIRED = 60
35 STATUS_TRANSIENT_CERT_REQUEST = 61
36 STATUS_AUTH_CERT_REQUIRED = 62
37 STATUS_CERT_REJECTED = 63
38 STATUS_FUTURE_CERT_REJECTED = 64
39 STATUS_EXPIRED_CERT_REJECTED = 65
40)
41
42const (
43 MIME_GEMINI = "text/gemini"
44 DEFAULT_MIME = MIME_GEMINI
45 DEFAULT_CHARSET = "utf-8"
46)
47
48var (
49 HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$")
50 MimeTypePattern = regexp.MustCompile("^[-\\w.]+/[-\\w.]+")
51 MimeCharsetPattern = regexp.MustCompile("charset=([^ ;]+)")
52 GeminiLinkPattern = regexp.MustCompile("^=>[ \\t]*([^ ]+)(?:[ \\t]+(.*))?$")
53)
54
55type GeminiHeader struct {
56 Status int
57 Meta string
58}
59
60type GeminiResponse struct {
61 Header *GeminiHeader
62 Body io.Reader
63}
64
65func GeminiGet(uri string) (*GeminiResponse, error) {
66 u, err := url.Parse(uri)
67 if err != nil {
68 return nil, err
69 }
70
71 if u.Scheme != "gemini" {
72 return nil, errors.New("invalid scheme for uri")
73 }
74
75 var (
76 host string
77 port int
78 )
79
80 hostport := strings.Split(u.Host, ":")
81 if len(hostport) == 2 {
82 host = hostport[0]
83 n, err := strconv.ParseInt(hostport[1], 10, 32)
84 if err != nil {
85 return nil, err
86 }
87 port = int(n)
88 } else {
89 host, port = hostport[0], 1965
90 }
91
92 conn, err := tls.Dial("tcp", host+":"+strconv.Itoa(port), &tls.Config{
93 MinVersion: tls.VersionTLS12,
94 InsecureSkipVerify: true,
95 })
96 if err != nil {
97 return nil, err
98 }
99
100 _, err = conn.Write([]byte(u.String() + CRLF))
101 if err != nil {
102 conn.Close()
103 return nil, err
104 }
105
106 reader := bufio.NewReader(conn)
107
108 line, _, err := reader.ReadLine()
109 if err != nil {
110 conn.Close()
111 return nil, err
112 }
113
114 header, err := ParseGeminiHeader(string(line))
115 if err != nil {
116 conn.Close()
117 return nil, err
118 }
119
120 return &GeminiResponse{
121 Header: header,
122 Body: reader,
123 }, nil
124}
125
126func ParseGeminiHeader(line string) (header *GeminiHeader, err error) {
127 matches := HeaderPattern.FindStringSubmatch(line)
128
129 status, err := strconv.Atoi(matches[1])
130 if err != nil {
131 return nil, err
132 }
133
134 meta := matches[2]
135
136 if int(status/10) == 2 {
137 if meta == "" {
138 meta = DEFAULT_MIME + ";charset=" + DEFAULT_CHARSET
139 }
140
141 mimeType := MimeTypePattern.FindString(meta)
142 if strings.HasPrefix(mimeType, "text/") && MimeCharsetPattern.FindString(meta) == "" {
143 meta += ";charset=" + DEFAULT_CHARSET
144 }
145 }
146
147 header = &GeminiHeader{
148 Status: status,
149 Meta: meta,
150 }
151
152 return
153}
diff --git a/template.go b/template.go
index 54aa53a..b7d1b1f 100644
--- a/template.go
+++ b/template.go
@@ -27,6 +27,7 @@ var tpltext = `<!doctype html>
27 <body class="{{ if not .Lines }}is-plain{{ end }}"> 27 <body class="{{ if not .Lines }}is-plain{{ end }}">
28 <header class="header header-base"> 28 <header class="header header-base">
29 <div class="location"> 29 <div class="location">
30 {{- $page := . -}}
30 {{- $href := "" -}} 31 {{- $href := "" -}}
31 {{- $uriParts := split .URI "/" -}} 32 {{- $uriParts := split .URI "/" -}}
32 {{- $uriLast := $uriParts | last -}} 33 {{- $uriLast := $uriParts | last -}}
@@ -35,35 +36,32 @@ var tpltext = `<!doctype html>
35 {{- $uriLast = $uriParts | last -}} 36 {{- $uriLast = $uriParts | last -}}
36 {{- $uriParts = $uriParts | pop -}} 37 {{- $uriParts = $uriParts | pop -}}
37 {{- end -}} 38 {{- end -}}
38 {{- if eq (len $uriParts) 1 -}}
39 {{- $uriLast = $uriParts | last -}}
40 {{- $uriParts = $uriParts | pop -}}
41 {{- end -}}
42 39
43 <button class="location__prefix">gopher://</button><button class="location__prefix location__prefix--mobile">://</button> 40 <a class="location__prefix">{{ .Protocol }}://</a><a class="location__prefix location__prefix--mobile">://</a>
41 {{- $href = printf "%s/%s" $href .Protocol -}}
44 {{- range $i, $part := $uriParts -}} 42 {{- range $i, $part := $uriParts -}}
45 {{- if ne $i 1 -}} 43 {{- if and (eq $page.Protocol "gopher") (eq $i 1) -}}
46 {{- $href = printf "%s/%s" $href . -}}
47 {{- if ne $i 0 -}}
48 <span class="location__slash">/</span>
49 {{- end -}}
50 <a href="{{ $href }}" class="location__uripart">{{ . }}</a>
51 {{- else -}}
52 {{- $href = printf "%s/1" $href -}} 44 {{- $href = printf "%s/1" $href -}}
53 {{- $part = $part | trimLeftChar -}} 45 {{- $part = $part | trimLeftChar -}}
54 {{- if not (eq $part "") -}} 46 {{- if not (eq $part "") -}}
55 {{- $href = printf "%s/%s" $href $part -}} 47 {{- $href = printf "%s/%s" $href $part -}}
56 <span class="location__slash">/</span><a href="{{ $href }}" class="location__uripart">{{ $part }}</a> 48 <span class="location__slash">/</span><a href="{{ $href }}/" class="location__uripart">{{ $part }}</a>
49 {{- end -}}
50 {{- else -}}
51 {{- $href = printf "%s/%s" $href . -}}
52 {{- if ne $i 0 -}}
53 <span class="location__slash">/</span>
57 {{- end -}} 54 {{- end -}}
55 <a href="{{ $href }}/" class="location__uripart">{{ . }}</a>
58 {{- end -}} 56 {{- end -}}
59 {{- end -}} 57 {{- end -}}
60 {{- if ne (len $uriParts) 0 -}} 58 {{- if ne (len $uriParts) 0 -}}
61 <span class="location__slash">/</span> 59 <span class="location__slash">/</span>
62 {{- end -}} 60 {{- end -}}
63 <span class="location__uripart">{{ $uriLast -}}</span> 61 <span class="location__uripart">{{ $uriLast }}</span>
64 </div> 62 </div>
65 <div class="actions"> 63 <div class="actions">
66 {{- if and (not .Lines) (not .Error) -}} 64 {{- if and (not .Lines) (not .Error) (eq .Protocol "gopher") -}}
67 <div class="action"><a href="/{{ .URI | replace "^(.*?)/0" "$1/9" }}">View raw</a></div> 65 <div class="action"><a href="/{{ .URI | replace "^(.*?)/0" "$1/9" }}">View raw</a></div>
68 {{- end -}} 66 {{- end -}}
69 <div class="action"><button class="settings-btn">Settings</button></div> 67 <div class="action"><button class="settings-btn">Settings</button></div>