diff options
author | Feuerfuchs <git@feuerfuchs.dev> | 2019-11-16 00:55:25 +0100 |
---|---|---|
committer | Feuerfuchs <git@feuerfuchs.dev> | 2019-11-16 00:55:25 +0100 |
commit | 9bed6157351eea6fdbccff69aba5cc967a0b2a56 (patch) | |
tree | e7b555078e7a4593f3e3456121dc8b04ae7b10a1 | |
parent | Only enable max with if wrapping is enabled (diff) | |
download | gopherproxy-9bed6157351eea6fdbccff69aba5cc967a0b2a56.tar.gz gopherproxy-9bed6157351eea6fdbccff69aba5cc967a0b2a56.tar.bz2 gopherproxy-9bed6157351eea6fdbccff69aba5cc967a0b2a56.zip |
Initial Gemini support
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | assets/main.js | 2 | ||||
-rw-r--r-- | assets/style.css | 2 | ||||
-rw-r--r-- | cmd/gopherproxy/main.go | 2 | ||||
-rw-r--r-- | css/main.scss | 1 | ||||
-rw-r--r-- | gopherproxy.go | 261 | ||||
-rw-r--r-- | js/main.ts | 12 | ||||
-rw-r--r-- | libgemini.go | 153 | ||||
-rw-r--r-- | template.go | 28 |
9 files changed, 402 insertions, 64 deletions
@@ -1,11 +1,10 @@ | |||
1 | # Gopher (RFC 1436) Web Proxy | 1 | # Gopher (RFC 1436) Web Proxy |
2 | 2 | ||
3 | gopherproxy is a Gopher (RFC 1436) Web Proxy that acts as a gateway into Gopherspace | 3 | gopherproxy 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. |
4 | by proxying standard Web HTTP requests to Gopher requests of the target server. | ||
5 | 4 | ||
6 | gopherproxy is a fork of [https://github.com/prologic/gopherproxy](https://github.com/prologic/gopherproxy). | 5 | gopherproxy is a fork of [https://github.com/prologic/gopherproxy](https://github.com/prologic/gopherproxy). |
7 | 6 | ||
8 | Demo: https://gopher.vulpes.one/ | 7 | Demo: 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 @@ | |||
1 | package gopherproxy | 1 | package gopherproxy |
2 | 2 | ||
3 | import ( | 3 | import ( |
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 | ||
29 | const ( | ||
30 | ITEM_TYPE_GEMINI_LINE = "" | ||
31 | ITEM_TYPE_GEMINI_LINK = " =>" | ||
32 | ) | ||
33 | |||
28 | type Item struct { | 34 | type 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 | ||
43 | func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { | 49 | func 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 | |||
111 | func 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 | |||
172 | func 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 | |||
109 | func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { | 183 | func 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 | ||
286 | func 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. |
210 | func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { | 389 | func 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))) |
@@ -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 @@ | |||
1 | package gopherproxy | ||
2 | |||
3 | import ( | ||
4 | "bufio" | ||
5 | "crypto/tls" | ||
6 | "errors" | ||
7 | "io" | ||
8 | "net/url" | ||
9 | "regexp" | ||
10 | "strconv" | ||
11 | "strings" | ||
12 | ) | ||
13 | |||
14 | const ( | ||
15 | CRLF = "\r\n" | ||
16 | ) | ||
17 | |||
18 | const ( | ||
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 | |||
42 | const ( | ||
43 | MIME_GEMINI = "text/gemini" | ||
44 | DEFAULT_MIME = MIME_GEMINI | ||
45 | DEFAULT_CHARSET = "utf-8" | ||
46 | ) | ||
47 | |||
48 | var ( | ||
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 | |||
55 | type GeminiHeader struct { | ||
56 | Status int | ||
57 | Meta string | ||
58 | } | ||
59 | |||
60 | type GeminiResponse struct { | ||
61 | Header *GeminiHeader | ||
62 | Body io.Reader | ||
63 | } | ||
64 | |||
65 | func 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 | |||
126 | func 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> |