├── .env ├── .gitignore ├── LICENSE ├── README.md ├── bun.lockb ├── config.nginx ├── main.py ├── package.json ├── public ├── cat.jpg ├── favicon.ico ├── index.html ├── input.html └── robots.txt ├── requirements.txt ├── screenshot.png ├── src ├── App.css ├── App.js ├── config.json ├── index.css ├── index.js └── logo.svg └── style.css /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_VERSION=$npm_package_version -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # gemini 26 | .known_hosts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 koyu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GemProxy 2 | 3 | GemProxy is a simple and stylish web proxy for the [Gemini network](https://gemini.circumlunar.space) primarily written in Bottle and React. It's easy to use and also self-hostable. 4 | 5 | ## Installation 6 | 7 | ### Backend 8 | 9 | Install Python3 alongside PIP and run the following command to install the dependencies: 10 | 11 | `sudo pip3 install -r requirements.txt` 12 | 13 | Afterwards run `main.py` to run the backend. 14 | 15 | ### Frontend 16 | 17 | Install NodeJS with yarn and run `yarn install` to install the dependencies. Change the backend variable in `src/config.json` to your needs and run `yarn build` to build the frontend. You can also debug the frontend using `yarn start` which will open a browser with auto-reload and error reporting. 18 | 19 | ## Caveats 20 | 21 | Due to the software's nature of not being multi-threaded it's recommended to set up a cronjob that restarts the backend every few minutes to avoid potential hangs. 22 | 23 | ## Screenshot 24 | 25 | ![Screenshot](screenshot.png) 26 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koyuawsmbrtn/gemproxy/5f5e55d554634638e04fe95804e76092b2f1e7c2/bun.lockb -------------------------------------------------------------------------------- /config.nginx: -------------------------------------------------------------------------------- 1 | server { 2 | root /var/www/gemproxy/build/; 3 | 4 | index index.html index.htm index.nginx-debian.html; 5 | 6 | server_name example.com; 7 | 8 | location / { 9 | try_files $uri $uri/ @index; 10 | } 11 | 12 | location @index { 13 | rewrite ^ /index.html break; 14 | } 15 | 16 | location /static/ { 17 | try_files $uri $uri/ =404; 18 | } 19 | 20 | location /api/v1/ { 21 | client_max_body_size 200M; 22 | proxy_set_header Host $host; 23 | proxy_set_header X-Real-IP $remote_addr; 24 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 25 | proxy_set_header X-Forwarded-Proto $scheme; 26 | proxy_pass http://localhost:1970; 27 | } 28 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import ignition 3 | from bottle import redirect, route, response, run 4 | import subprocess 5 | from protego import Protego 6 | 7 | rooturl = "//" 8 | 9 | @route("/") 10 | def index(): 11 | return "" 12 | 13 | @route("/api/v1/check") 14 | def check(): 15 | response.content_type = "text/plain" 16 | response.headers['Access-Control-Allow-Origin'] = '*' 17 | return "OK" 18 | 19 | @route("/api/v1/get/") 20 | def defr(url): 21 | req = ignition.request(rooturl+url.replace("$", "?")) 22 | response.headers['Access-Control-Allow-Origin'] = '*' 23 | isImage = False 24 | if ".jpg" in url or ".png" in url or "favicon.txt" in url: 25 | response.headers["Cache-Control"] = "public, max-age=604800" 26 | images = [".jpg", ".png", ".gif", ".ico", ".jpeg"] 27 | for i in images: 28 | if i in str(req.url): 29 | response.content_type = "image/"+str(req.url.split(".")[1:][1]) 30 | isImage = True 31 | if str(req).split(" ")[0].startswith("1"): 32 | return "$$$input$$$"+str(req).split("\n")[0].replace(str(req).split("\n")[0].split(" ")[0], "") 33 | else: 34 | if str(req).split(" ")[0].startswith("5") or str(req).split(" ")[0].startswith("0") or str(req).split(" ")[0].startswith("4"): 35 | return "# Error "+str(req).split("\n")[0].split(" ")[0]+"\n"+str(req).split("\n")[0].replace(str(req).split("\n")[0].split(" ")[0], "") 36 | else: 37 | robots = ignition.request(rooturl+url.replace("$", "?").split("/")[0]+"/robots.txt") 38 | rp = Protego.parse(str(robots.raw_body).replace("\\n", "\n").replace("b'", "").replace("'", "")) 39 | if rp.can_fetch(rooturl+url.replace("$", "?"), rooturl+url.replace("$", "?")): 40 | if not str(req.raw_body) == "": 41 | if isImage: 42 | return req.raw_body 43 | else: 44 | return "\n".join(str(req).split("\n")[1:]).replace("=> ", "=>").replace("=>", "=> ") 45 | else: 46 | redirect("/get/"+url+"/") 47 | else: 48 | return "# Error\nThis capsule has requested not to be accessed through a Gemini proxy via the robots.txt file." 49 | 50 | @route("/api/v1/gitid") 51 | def gitid(): 52 | response.headers['Access-Control-Allow-Origin'] = '*' 53 | response.content_type = "text/plain" 54 | return str(subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])).replace("b'", "").replace("\\n'", "") 55 | 56 | run(host="127.0.0.1", port=1970, server="tornado") 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gemproxy", 3 | "version": "2.3.1", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^6.5.0", 7 | "@testing-library/react": "^14.3.1", 8 | "@testing-library/user-event": "^14.5.2", 9 | "@twemoji/api": "^15.1.0", 10 | "gemini-to-html": "^2.2.0", 11 | "jquery": "^3.7.1", 12 | "react": "^18.3.1", 13 | "react-dom": "^18.3.1", 14 | "react-scripts": "^5.0.1", 15 | "web-vitals": "^3.5.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "dev": "react-scripts start", 20 | "build": "react-scripts build" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "trustedDependencies": [ 41 | "core-js", 42 | "core-js-pure" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /public/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koyuawsmbrtn/gemproxy/5f5e55d554634638e04fe95804e76092b2f1e7c2/public/cat.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koyuawsmbrtn/gemproxy/5f5e55d554634638e04fe95804e76092b2f1e7c2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | GemProxy 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /public/input.html: -------------------------------------------------------------------------------- 1 |

Input

2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ignition-gemini 2 | bottle 3 | tornado 4 | markdown 5 | protego -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koyuawsmbrtn/gemproxy/5f5e55d554634638e04fe95804e76092b2f1e7c2/screenshot.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | } 4 | 5 | body { 6 | background: #333; 7 | color: #ccc; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | padding: 10px; 11 | padding-top: 20px !important; 12 | overflow-x: hidden; 13 | } 14 | 15 | #content { 16 | background: #333; 17 | border-radius: 10px; 18 | padding: 20px; 19 | } 20 | 21 | img:not(img[class="emoji"]) { 22 | border-radius: 10px; 23 | margin-top: 10px; 24 | margin-bottom: 10px; 25 | } 26 | 27 | hr { 28 | margin-top: 20px; 29 | border: 0; 30 | border-bottom: 1px solid #aaa; 31 | } 32 | 33 | a, a:visited { 34 | color: #ccc; 35 | } 36 | 37 | a:hover { 38 | text-decoration: none; 39 | } 40 | 41 | .emoji { 42 | width: 16px !important; 43 | height: 16px !important; 44 | vertical-align: middle !important; 45 | } 46 | 47 | code > p { 48 | margin: 0; 49 | line-height: 0; 50 | } 51 | 52 | a { 53 | text-decoration: none; 54 | border-bottom: 1px solid #ccc; 55 | } 56 | 57 | a:hover { 58 | text-decoration: none; 59 | border-bottom: 0; 60 | } 61 | 62 | #content { 63 | word-wrap: break-word; 64 | } 65 | 66 | h2 { 67 | margin-top: 60px; 68 | margin-bottom: 0; 69 | font-weight: bold; 70 | background: #ccc; 71 | color: #333; 72 | max-width: 250px; 73 | padding: 8px; 74 | text-align: center; 75 | border-radius: 10px; 76 | } 77 | 78 | .published { 79 | margin-top: -13px; 80 | padding-bottom: 10px; 81 | font-size: 8pt; 82 | } 83 | 84 | #proxyui { 85 | display: inline-flex; 86 | padding-left: 16px; 87 | } 88 | 89 | #favicon { 90 | padding-right: 6px; 91 | } 92 | 93 | #addressbar { 94 | width: 60vw !important; 95 | } 96 | 97 | @media(min-width:1600px) { 98 | body { 99 | width: 60%; 100 | position: relative; 101 | margin: 0 auto; 102 | background-position: bottom right; 103 | background-size: 300px; 104 | background-repeat: no-repeat; 105 | background-attachment: fixed; 106 | } 107 | 108 | #widgets { 109 | float: left; 110 | position: absolute; 111 | width: 250px; 112 | margin-left: -310px; 113 | top: 20px; 114 | } 115 | } 116 | 117 | @media(max-width:1600px) { 118 | #widgets { 119 | margin-top: 20px; 120 | } 121 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | /* eslint-disable no-script-url */ 3 | import React from 'react'; 4 | import './App.css'; 5 | import config from './config.json'; 6 | import $ from 'jquery'; 7 | import twemoji from '@twemoji/api'; 8 | 9 | const parse = require('gemini-to-html/parse') 10 | const render = require('gemini-to-html/render') 11 | const contentCss = ""; 12 | 13 | export default class App extends React.Component { 14 | componentDidMount() { 15 | function mobileCheck() { 16 | let check = false; 17 | // eslint-disable-next-line no-useless-escape 18 | (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); 19 | return check; 20 | }; 21 | //Git info 22 | $.get(config.backend+"api/v1/gitid", function(data) { 23 | $("#git-id").html(data); 24 | }); 25 | $.ajax({ 26 | url: config.backend+"api/v1/check", 27 | success: function() { 28 | $("#gitid").show(); 29 | $("#proxiedfrom").show(); 30 | $("#content").removeAttr("style"); 31 | $("#header").show(); 32 | }, 33 | error: function() { 34 | $("#content").html("

Error!

\n

The backend server is currently unavailable. Please check back later!

\n

"); 35 | $("#content").attr("style", "text-align:center;"); 36 | $("#gitid").hide(); 37 | $("#proxiedfrom").hide(); 38 | $("#header").hide(); 39 | }, 40 | timeout: 30000 41 | }); 42 | //Current Gemini URL 43 | var gemurl = window.location.href.split("/")[3]; 44 | var slashcount = window.location.href.split("/").length - 1; 45 | if (slashcount === 3) { 46 | window.location.href = "/koyu.space/"; 47 | } 48 | if (slashcount > 2) { 49 | $("#addressbar").attr("value", "gemini:/"+window.location.href.replaceAll(window.location.protocol+"//"+window.location.host, "")); 50 | } else { 51 | $("#addressbar").attr("value", "gemini://koyu.space/"+gemurl); 52 | } 53 | if (!$("#addressbar").val().includes(".")) { 54 | $("#addressbar").val($("#addressbar").val().replaceAll("gemini://", "gemini://koyu.space/")); 55 | } 56 | if ($("#addressbar").val().split("/").length - 1 < 3) { 57 | window.location.href = window.location.href+"/"; 58 | } 59 | $("#gemurl").attr("href", $("#addressbar").val()); 60 | $("#gemurl").html($("#addressbar").val()); 61 | if (!$("#addressbar").val().replaceAll("gemini://", "").includes(".jpg") && !$("#addressbar").val().replaceAll("gemini://", "").includes(".png") && !$("#addressbar").val().replaceAll("gemini://", "").includes(".jpeg")) { 62 | $.get(config.backend+"api/v1/get/"+$("#addressbar").val().replaceAll("gemini://", "").split("/")[0]+"/favicon.txt", function(data) { 63 | $("#gitid").show(); 64 | $("#proxiedfrom").show(); 65 | $("#content").removeAttr("style"); 66 | if (!data.includes("# Error ")) { 67 | $("#favicon").html(data); 68 | $("#favicon").html(twemoji.parse($("#favicon").html())); 69 | } 70 | }); 71 | $.get(config.backend+"api/v1/get/"+$("#addressbar").val().replaceAll("gemini://", "").replaceAll("?", "$"), function(data) { 72 | if (!data.startsWith("$$$input$$$")) { 73 | var parsed = parse(data); 74 | var content = ""; 75 | if ($("#addressbar").val().replaceAll("gemini://", "").split("/")[0] === "koyu.space") { 76 | content = render(parsed).replaceAll("href=\"/", "href=\""); 77 | } else { 78 | content = render(parsed).replaceAll("href=\"/", "href=\"/"+$("#addressbar").val().replaceAll("gemini://", "").split("/")[0]+"/"); 79 | } 80 | //Open external links in new tab 81 | content = content.replaceAll("href=\"https://", "target=\"_blank\" href=\"https://"); 82 | //Output page 83 | $("#content").html(content); 84 | if (!mobileCheck()) { 85 | $("head").append(contentCss); 86 | } 87 | window.setTimeout(function() { 88 | //Parse URLs 89 | $('#content a[href*="gemini://"]').each(function() { 90 | $(this).attr("href", $(this).attr("href").replaceAll("gemini://", "/")); 91 | }); 92 | }) 93 | //Display inline-images 94 | $('a[href*=".jpg"]').each(function() { 95 | var styles = "width:100%"; 96 | if ($(this).html().includes("_right")) { 97 | styles = "float:right;padding:5px;"; 98 | } 99 | var imguri = new URL($(this).attr("href"), config.backend+"api/v1/get/"+$("#addressbar").val().replaceAll("gemini://", "").replaceAll("?", "$")).href; 100 | if (!imguri.includes("/api/v1/get/")) { 101 | imguri = imguri.replaceAll(config.backend, config.backend+"api/v1/get/"); 102 | } 103 | $(this).html(""); 104 | $(this).attr("target", "_blank"); 105 | $(this).attr("href", imguri); 106 | $(this).attr("style", "border:0;"); 107 | }); 108 | $('a[href*=".jpeg"]').each(function() { 109 | var styles = "width:100%"; 110 | if ($(this).html().includes("_right")) { 111 | styles = "float:right;padding:5px;"; 112 | } 113 | var imguri = new URL($(this).attr("href"), config.backend+"api/v1/get/"+$("#addressbar").val().replaceAll("gemini://", "").replaceAll("?", "$")).href; 114 | if (!imguri.includes("/api/v1/get/")) { 115 | imguri = imguri.replaceAll(config.backend, config.backend+"api/v1/get/"); 116 | } 117 | $(this).html(""); 118 | $(this).attr("target", "_blank"); 119 | $(this).attr("href", imguri); 120 | $(this).attr("style", "border:0;"); 121 | }); 122 | $('a[href*=".png"]').each(function() { 123 | var styles = "width:100%"; 124 | if ($(this).html().includes("_right")) { 125 | styles = "float:right;padding:5px;"; 126 | } 127 | var imguri = new URL($(this).attr("href"), config.backend+"api/v1/get/"+$("#addressbar").val().replaceAll("gemini://", "").replaceAll("?", "$")).href; 128 | if (!imguri.includes("/api/v1/get/")) { 129 | imguri = imguri.replaceAll(config.backend, config.backend+"api/v1/get/"); 130 | } 131 | $(this).html(""); 132 | $(this).attr("target", "_blank"); 133 | $(this).attr("href", imguri); 134 | $(this).attr("style", "border:0;"); 135 | }); 136 | } else { 137 | $.get(config.backend+"api/v1/get/"+$("#addressbar").val().replaceAll("gemini://", "").replaceAll("?", "$"), function(cnt) { 138 | $.get("/input.html", function(data) { 139 | $("#content").html(data.replaceAll("%title%", cnt.replaceAll("$$$input$$$ ", "").replaceAll("$$$input$$$", ""))); 140 | $("#inputtext").focus(); 141 | $("#inputtext").keypress((e) => { 142 | if (e.which === 13) { 143 | if ($("#inputtext").val().replaceAll(" ", "") !== "") { 144 | window.location.href = window.location.href+"?"+$("#inputtext").val(); 145 | e.preventDefault(); 146 | } 147 | } 148 | }); 149 | }); 150 | }); 151 | } 152 | $("#content").html(twemoji.parse($("#content").html())); 153 | if ($("h1").html() !== undefined) { 154 | document.title = $("h1").html()+" - GemProxy"; 155 | } 156 | }); 157 | } else { 158 | $("html").html(""); 159 | } 160 | //Address bar handler 161 | $("#addressbar").keypress((e) => { 162 | if (e.which === 13) { 163 | if ($("#addressbar").val().replaceAll(" ", "") !== "") { 164 | var newlocation = "/"+$("#addressbar").val().replace("gemini://", "")+"/"; 165 | window.location.href = newlocation.replaceAll("//", "/"); 166 | e.preventDefault(); 167 | } 168 | } 169 | }) 170 | } 171 | 172 | render() { 173 | return ( 174 |
175 | 181 |
182 | Loading... 183 |
184 |
185 |

♊️ Proxied content from gemini://koyu.space

186 |

GemProxy v{process.env.REACT_APP_VERSION} () | Source code | Donate

187 |
188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "backend": "http://localhost:1970/" 3 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | } 4 | 5 | body { 6 | background: #223; 7 | color: #ccc; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | padding: 10px; 11 | padding-top: 20px !important; 12 | } 13 | 14 | #content { 15 | background: #223; 16 | border-radius: 10px; 17 | padding: 20px; 18 | } 19 | 20 | img:not(img[class="emoji"]) { 21 | border-radius: 10px; 22 | } 23 | 24 | hr { 25 | margin-top: 20px; 26 | border: 0; 27 | border-bottom: 1px solid #aaa; 28 | } 29 | 30 | a, a:visited { 31 | color: #ccc; 32 | } 33 | 34 | a:hover { 35 | text-decoration: none; 36 | } 37 | 38 | .emoji { 39 | width: 16px !important; 40 | height: 16px !important; 41 | vertical-align: middle !important; 42 | } 43 | 44 | code > p { 45 | margin: 0; 46 | line-height: 0; 47 | } 48 | 49 | #content a { 50 | display: block; 51 | } 52 | 53 | #content { 54 | word-wrap: break-word; 55 | } 56 | 57 | h2 { 58 | margin-top: 60px; 59 | margin-bottom: 0; 60 | font-weight: bold; 61 | background: #ccc; 62 | color: #223; 63 | max-width: 250px; 64 | padding: 8px; 65 | text-align: center; 66 | border-radius: 10px; 67 | } 68 | 69 | .published { 70 | margin-top: -13px; 71 | padding-bottom: 10px; 72 | font-size: 8pt; 73 | } 74 | 75 | #proxyui { 76 | display: inline-flex; 77 | padding-left: 16px; 78 | } 79 | 80 | #favicon { 81 | padding-right: 6px; 82 | } 83 | 84 | #addressbar { 85 | width: 60vw !important; 86 | } 87 | 88 | a .fa { 89 | text-decoration: underline; 90 | } 91 | 92 | a:hover .fa { 93 | text-decoration: none; 94 | } 95 | 96 | @media(min-width:1600px) { 97 | body { 98 | width: 60%; 99 | position: relative; 100 | margin: 0 auto; 101 | background-position: bottom right; 102 | background-size: 300px; 103 | background-repeat: no-repeat; 104 | background-attachment: fixed; 105 | } 106 | 107 | #widgets { 108 | float: left; 109 | position: absolute; 110 | width: 250px; 111 | margin-left: -310px; 112 | top: 20px; 113 | } 114 | } 115 | 116 | @media(max-width:1600px) { 117 | #widgets { 118 | margin-top: 20px; 119 | } 120 | } --------------------------------------------------------------------------------