├── Dockerfile ├── LICENSE ├── README.md ├── gimmeasearx.go ├── go.mod ├── go.sum ├── internal ├── grade │ └── grade.go ├── instances │ └── instances.go └── version │ └── version.go ├── opensearch.xml ├── packages └── PKGBUILD ├── screenshots ├── 1.png └── 2.png ├── services ├── openrc │ └── openrc-service └── systemd │ └── gimmeasearx.service └── templates └── index.html /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/golang:1.13-buster as builder 2 | RUN mkdir /build 3 | ADD . /build/ 4 | WORKDIR /build 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o main ./gimmeasearx.go 6 | FROM gcr.io/distroless/base-debian10 7 | COPY --from=builder /build/main /app/ 8 | COPY --from=builder /build/templates /app/templates 9 | COPY opensearch.xml /app/opensearch.xml 10 | WORKDIR /app 11 | CMD ["./main"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | gimmeasearx - find a random searx instance 2 | Copyright (C) 2020-2021 demostanis worlds 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gimmeasearx 2 | 3 | Configurable, JavaScript-less Neocities alternative, written in Go! 4 | It gives you a random searx (privacy-respecting metasearch engine) instance each time you visit the page. 5 | You can either clone, build and use it locally using techniques below or use the Tor [hidden service](http://7tcuoi57curagdk7nsvmzedcxgwlrq2d6jach4ksa3vj72uxrzadmqqd.onion/). There's also [a wiki](https://github.com/demostanis/gimmeasearx/wiki)! 6 | 7 | ![screenshot](screenshots/2.png) 8 | 9 | ## Running 10 | You will need `git` and `go`. Once setup, run the following commands: 11 | ```sh 12 | git clone https://github.com/demostanis/gimmeasearx.git 13 | go run gimmeasearx.go 14 | ``` 15 | That's it! Open up a browser and check [localhost:8080](http://localhost:8080). 16 | 17 | If you want .onion instances to show up, you need [Tor](https://www.torproject.org/) installed and running. 18 | 19 | For more ways to run gimmeasearx, such as with Docker or through a systemd service, check the [wiki page](https://github.com/demostanis/gimmeasearx/wiki/Installing-&-Running). 20 | 21 | -------- 22 | 23 | Licensed under GPLv3. 24 | 25 | If my time spent coding this was helpful to you, 26 | I'd be gladful to receive donations: 27 | 28 | - Ethereum: **0xF239e7C7b1C75EFF467EE4b74CEB4002E3d00BEE** 29 | 30 | - Bitcoin: **5cc720fb7ca0bf0807e0223946fae738** 31 | 32 | -------------------------------------------------------------------------------- /gimmeasearx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/demostanis/gimmeasearx/internal/grade" 6 | "github.com/demostanis/gimmeasearx/internal/instances" 7 | findlatestversion "github.com/demostanis/gimmeasearx/internal/version" 8 | "github.com/hashicorp/go-version" 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo/v4/middleware" 11 | "html/template" 12 | "io" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strings" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | // Used by echo. 22 | type Template struct { 23 | templates *template.Template 24 | } 25 | 26 | // Used by echo to render templates. 27 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 28 | return t.templates.ExecuteTemplate(w, name, data) 29 | } 30 | 31 | var t = &Template{ 32 | templates: template.Must(template.ParseGlob("templates/*.html")), 33 | } 34 | 35 | var fetchedInstances *map[string]instances.Instance = nil 36 | 37 | func main() { 38 | e := echo.New() 39 | e.Renderer = t 40 | 41 | var fetch func() 42 | fetch = func() { 43 | resp, err := instances.Fetch() 44 | if err != nil { 45 | fmt.Printf("Error: %s\n", err) 46 | os.Exit(1) 47 | } 48 | fetchedInstances = &resp.Instances 49 | var mutex = &sync.Mutex{} 50 | for key, instance := range *fetchedInstances { 51 | go func(key string, instance instances.Instance) { 52 | result := instances.Verify(key, instance) 53 | if !result { 54 | mutex.Lock() 55 | delete(*fetchedInstances, key) 56 | mutex.Unlock() 57 | } 58 | }(key, instance) 59 | } 60 | } 61 | 62 | fetch() 63 | go func() { 64 | for range time.Tick(time.Hour * 24) { 65 | fetch() 66 | } 67 | }() 68 | 69 | e.Use(middleware.Gzip()) 70 | e.Use(middleware.Recover()) 71 | 72 | e.GET("/", index) 73 | e.GET("/search", search) 74 | e.File("/opensearch.xml", "opensearch.xml") 75 | 76 | port, exists := os.LookupEnv("PORT") 77 | if !exists { 78 | port = ":8080" 79 | } 80 | host, exists := os.LookupEnv("HOST") 81 | if !exists { 82 | host = "localhost" 83 | } 84 | e.Logger.Fatal(e.Start(host + port)) 85 | } 86 | 87 | func search(c echo.Context) error { 88 | params := parseParams(c) 89 | torOnlyEnabled := params.torOnlyEnabled 90 | torEnabled := params.torEnabled 91 | gradesEnabled := params.gradesEnabled 92 | blacklist := params.blacklist 93 | preferences := params.preferences 94 | queryParams := params.params 95 | minVersion := params.minVersion 96 | customInstances := params.customInstances 97 | 98 | randUrl, _ := instances.FindRandomInstance(fetchedInstances, gradesEnabled, blacklist, torEnabled, torOnlyEnabled, minVersion, customInstances) 99 | if randUrl == nil { 100 | return c.Render(http.StatusExpectationFailed, "index.html", map[string]bool{ 101 | "Error": true, 102 | }) 103 | } 104 | 105 | if fetchedInstances != nil { 106 | finalUrl := *randUrl 107 | finalUrl += "?q=" + url.QueryEscape(c.QueryParam("q")) 108 | if len(preferences) > 0 { 109 | finalUrl += "&preferences=" + url.QueryEscape(preferences) 110 | } 111 | if len(queryParams) > 0 { 112 | for _, param := range queryParams { 113 | finalUrl += "&" + param.Name + "=" + param.Value 114 | } 115 | } 116 | return c.Redirect(http.StatusFound, finalUrl) 117 | } else { 118 | return c.String(http.StatusTooEarly, "No instances available. Please try again in a few seconds.") 119 | } 120 | } 121 | 122 | func index(c echo.Context) error { 123 | params := parseParams(c) 124 | torOnlyEnabled := params.torOnlyEnabled 125 | torEnabled := params.torEnabled 126 | gradesEnabled := params.gradesEnabled 127 | blacklist := params.blacklist 128 | preferences := params.preferences 129 | queryParams := params.params 130 | minVersion := params.minVersion 131 | latestVersion := params.latestVersion 132 | customInstances := params.customInstances 133 | 134 | queryParamsRaw := "" 135 | for i, param := range queryParams { 136 | if i != 0 { 137 | queryParamsRaw += "&" 138 | } 139 | queryParamsRaw += fmt.Sprintf("%s=%s", param.Name, param.Value) 140 | } 141 | 142 | data := map[string]interface{}{ 143 | "CurrentUrl": c.Request().URL.RequestURI(), 144 | "OptionsSelected": map[string]interface{}{ 145 | "Tor": torEnabled, 146 | "TorOnly": torOnlyEnabled, 147 | "Blacklist": blacklist, 148 | "Latest": latestVersion, 149 | }, 150 | "Grades": grade.Grades(), 151 | "GradesSelected": gradesEnabled, 152 | "Preferences": preferences, 153 | "Params": queryParams, 154 | "ParamsRaw": queryParamsRaw, 155 | "MinVersion": minVersion.Original(), 156 | "CustomInstances": customInstances, 157 | } 158 | 159 | if fetchedInstances != nil { 160 | randUrl, isCustom := instances.FindRandomInstance(fetchedInstances, gradesEnabled, blacklist, torEnabled, torOnlyEnabled, minVersion, customInstances) 161 | if randUrl == nil { 162 | data["Error"] = true 163 | return c.Render(http.StatusExpectationFailed, "index.html", data) 164 | } 165 | if isCustom { 166 | data["Instance"] = instances.Instance{Comments: []string{"Custom instance"}} 167 | data["InstanceUrl"] = randUrl 168 | } else { 169 | randInstance := (*fetchedInstances)[*randUrl] 170 | 171 | data["Instance"] = randInstance 172 | data["InstanceUrl"] = randUrl 173 | data["GradeComment"] = grade.Comment(randInstance.Html.Grade) 174 | } 175 | 176 | return c.Render(http.StatusOK, "index.html", data) 177 | } else { 178 | data["Error"] = true 179 | return c.Render(http.StatusTooEarly, "index.html", data) 180 | } 181 | } 182 | 183 | type queryParam struct { 184 | Name string 185 | Value string 186 | } 187 | 188 | // Params that may be specified in the URL. 189 | type Params struct { 190 | torEnabled bool 191 | torOnlyEnabled bool 192 | gradesEnabled []string 193 | blacklist []string 194 | preferences string 195 | params []queryParam 196 | minVersion version.Version 197 | latestVersion bool 198 | customInstances []string 199 | } 200 | 201 | func parseParams(c echo.Context) Params { 202 | torOnlyEnabled := c.QueryParam("toronly") == "on" 203 | torEnabled := torOnlyEnabled || c.QueryParam("tor") == "on" 204 | latestVersion := c.QueryParam("latestversion") == "on" 205 | minVersion, _ := version.NewVersion("0.0.0") 206 | if !latestVersion { 207 | r, err := version.NewVersion(c.QueryParam("minversion")) 208 | if err != nil { 209 | minVersion, _ = version.NewVersion("0.0.0") 210 | } else { 211 | minVersion = r 212 | } 213 | } else { 214 | minVersion, _ = version.NewVersion(findlatestversion.Searx()) 215 | } 216 | gradesEnabled := *new([]string) 217 | 218 | for _, thisGrade := range grade.Grades() { 219 | if c.QueryParam(thisGrade["Id"].(string)) == "on" { 220 | gradesEnabled = append(gradesEnabled, thisGrade["Id"].(string)) 221 | } 222 | } 223 | if len(gradesEnabled) < 1 { 224 | gradesEnabled = grade.Defaults() 225 | } 226 | 227 | blacklist := *new([]string) 228 | if b := c.QueryParam("blacklist"); len(b) > 0 { 229 | for _, s := range strings.Split(b, ";") { 230 | if strings.TrimSpace(s) != "" { 231 | blacklist = append(blacklist, strings.TrimSpace(s)) 232 | } 233 | } 234 | } 235 | 236 | customInstances := *new([]string) 237 | if b := c.QueryParam("custominstances"); len(b) > 0 { 238 | for _, s := range strings.Split(b, ";") { 239 | if strings.TrimSpace(s) != "" && strings.TrimSpace(s) != "https://myothercustom.instan.ce.ru.fr.es/" && strings.TrimSpace(s) != "https://mycustom.searx.instance/" { 240 | customInstances = append(customInstances, strings.TrimSpace(s)) 241 | } 242 | } 243 | } 244 | 245 | queryParams := *new([]queryParam) 246 | if p := c.QueryParam("params"); len(p) > 0 { 247 | query, err := url.QueryUnescape(p) 248 | if err == nil { 249 | values, err := url.ParseQuery(query) 250 | if err == nil { 251 | for name, value := range values { 252 | queryParams = append(queryParams, queryParam{ 253 | name, value[0], 254 | }) 255 | } 256 | } 257 | } 258 | } 259 | 260 | preferences := c.QueryParam("preferences") 261 | 262 | return Params{ 263 | torEnabled, 264 | torOnlyEnabled, 265 | gradesEnabled, 266 | blacklist, 267 | preferences, 268 | queryParams, 269 | *minVersion, 270 | latestVersion, 271 | customInstances, 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/demostanis/gimmeasearx 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/hashicorp/go-version v1.2.1 7 | github.com/labstack/echo/v4 v4.1.17 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 4 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 5 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 6 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 7 | github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= 8 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 9 | github.com/labstack/echo v1.4.4 h1:1bEiBNeGSUKxcPDGfZ/7IgdhJJZx8wV/pICJh4W2NJI= 10 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 11 | github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo= 12 | github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ= 13 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 14 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 15 | github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= 16 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 17 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 18 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 19 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 20 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 21 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 22 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 23 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 24 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 29 | github.com/tdewolff/minify v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw= 30 | github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo= 31 | github.com/tdewolff/minify/v2 v2.9.7 h1:r8ewdcX8VYUoNj+s9WSy4FtNNNqNPevWOkb/MksAtzQ= 32 | github.com/tdewolff/minify/v2 v2.9.7/go.mod h1:AcJ/ggtHex5N/QiafLI8rlIO3qwSlgbPNLi27VZSYz8= 33 | github.com/tdewolff/parse/v2 v2.5.4 h1:ggaQ1SVE8wErRrZwUs49I6iQ1zL/tFlb7KtYsk2I8Yk= 34 | github.com/tdewolff/parse/v2 v2.5.4/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= 35 | github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 36 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 37 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 38 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 39 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 40 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 43 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 44 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 45 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= 46 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 48 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 49 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 50 | golang.org/x/net v0.0.0-20201009032441-dbdefad45b89 h1:1GKfLldebiSdhTlt3nalwrb7L40Tixr/0IH+kSbRgmk= 51 | golang.org/x/net v0.0.0-20201009032441-dbdefad45b89/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= 62 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM= 65 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 68 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 69 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 72 | -------------------------------------------------------------------------------- /internal/grade/grade.go: -------------------------------------------------------------------------------- 1 | package grade 2 | 3 | // Returns an array of grade IDs and 4 | // their corresponding symbol. 5 | func Grades() []map[string]interface{} { 6 | return []map[string]interface{}{ 7 | { "Symbol": "V", "Id": "grade-v", }, 8 | { "Symbol": "C", "Id": "grade-c", }, 9 | { "Symbol": "Cjs", "Id": "grade-cjs", }, 10 | { "Symbol": "E", "Id": "grade-e", }, 11 | } 12 | } 13 | 14 | // Finds a grade ID's symbol. 15 | func Symbol(id string) string { 16 | var result string 17 | for _, thisGrade := range Grades() { 18 | if thisGrade["Id"] == id { 19 | result = thisGrade["Symbol"].(string) 20 | } 21 | } 22 | return result 23 | } 24 | 25 | // Default grades for people whom haven't set them. 26 | func Defaults() []string { 27 | return []string{"grade-v", "grade-c"} 28 | } 29 | 30 | // Returns a grade's comment which will be 31 | // shown in a tooltip. 32 | func Comment(grade string) string { 33 | switch grade { 34 | case "V": 35 | return `"Vanilla instance": all static files are the original ones from the searx source code` 36 | case "C": 37 | return "Some static files have been modified, but all scripts are the original ones from the searx source code" 38 | case "Cjs": 39 | return "Some static files have been modified, including scripts" 40 | case "E": 41 | return "Some files originate from another domain!" 42 | default: 43 | return "Unknown grade" 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /internal/instances/instances.go: -------------------------------------------------------------------------------- 1 | package instances 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "errors" 7 | "strconv" 8 | "github.com/demostanis/gimmeasearx/internal/grade" 9 | "github.com/hashicorp/go-version" 10 | "io/ioutil" 11 | "encoding/json" 12 | "math/rand" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "regexp" 17 | ) 18 | 19 | const USER_AGENT = "Mozilla/5.0 (gimmeasearx; https://github.com/demostanis/gimmeasearx; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 I am nice, I promise." 20 | 21 | // The data fetched from searx.space. 22 | type InstancesData struct { 23 | Instances map[string]Instance `json:"instances"` 24 | } 25 | // Struct representing an instance 26 | // in the data fetched. 27 | type Instance struct { 28 | Comments []string `json:"comments"` 29 | NetworkType *string `json:"network_type"` 30 | Error *string `json:"error,omit_empty"` 31 | Version *string `json:"version"` 32 | Html *struct { 33 | Resources struct {} `json:"ressources"` 34 | Grade string `json:"grade"` 35 | } `json:"html,omit_empty"` 36 | } 37 | 38 | // Creates an InstancesData from the fetched JSON data. 39 | func InstancesNew(data io.ReadCloser) (*InstancesData, error) { 40 | var instances InstancesData 41 | resp, err := ioutil.ReadAll(data) 42 | if err != nil { 43 | return nil, err 44 | } 45 | if err := json.Unmarshal(resp, &instances); err != nil { 46 | return nil, err 47 | } 48 | return &instances, nil 49 | } 50 | 51 | // Checks whether tor is listening on :9050 or :9150. 52 | func TorListening() (int, error) { 53 | _, err := net.Dial("tcp", ":9050") 54 | if err != nil { 55 | _, err2 := net.Dial("tcp", ":9150") 56 | if err2 != nil { 57 | return 0, errors.New("Tor is not listening") 58 | } else { 59 | return 9150, nil 60 | } 61 | } else { 62 | return 9050, nil 63 | } 64 | } 65 | 66 | // Verifies an instance, using tor or not, by checking if it 67 | // returns expected results from searches. It also removes 68 | // instances using Cloudflare. 69 | func Verify(instanceUrl string, instance Instance) bool { 70 | result := false 71 | useTor := false 72 | // We need other tests 73 | tests := map[string][]string{ 74 | "south+park": []string{"Trey Parker", "Matt Stone"}, 75 | "gimmeasearx": []string{"Find a random searx instance"}, 76 | } 77 | port, err := TorListening() 78 | if strings.HasSuffix(instanceUrl, ".onion/") && err == nil { 79 | useTor = true 80 | } 81 | for search, matches := range tests { 82 | var resp *http.Response 83 | var err error 84 | if useTor { 85 | req, _ := http.NewRequest("GET", instanceUrl + "search?q=" + search, nil) 86 | req.Header.Set("User-Agent", USER_AGENT) 87 | req.Header.Set("Accept-Language", "en-US,en;q=0.5") 88 | 89 | tr := &http.Transport{ 90 | Proxy: func(req *http.Request) (*url.URL, error) { 91 | return url.Parse("socks5://127.0.0.1:" + strconv.Itoa(port)) 92 | }, 93 | } 94 | client := &http.Client{Transport: tr} 95 | resp, err = client.Do(req) 96 | } else { 97 | req, _ := http.NewRequest("GET", instanceUrl + "search?q=" + search, nil) 98 | // These headers are mostly to circumvent filtron. 99 | // Please don't hate me for bypassing your anti rooboots. 100 | req.Header.Set("User-Agent", USER_AGENT) 101 | req.Header.Set("Accept-Language", "en-US,en;q=0.5") 102 | 103 | client := &http.Client{} 104 | resp, err = client.Do(req) 105 | } 106 | if err == nil && resp != nil { 107 | if resp.Header.Get("server") == "cloudflare" { 108 | result = false 109 | continue 110 | } 111 | page, _ := ioutil.ReadAll(resp.Body) 112 | for _, regex := range matches { 113 | r := regexp.MustCompile(regex) 114 | result = r.MatchString(string(page)) 115 | 116 | // Do not make any other test if one fails 117 | if !result { 118 | return result 119 | } 120 | } 121 | resp.Body.Close() 122 | } 123 | } 124 | return result 125 | } 126 | 127 | // Fetches data from searx.space. 128 | func Fetch() (*InstancesData, error) { 129 | resp, err := http.Get("https://searx.space/data/instances.json") 130 | defer resp.Body.Close() 131 | if err != nil { 132 | return nil, err 133 | } 134 | if resp.StatusCode != 200 { 135 | return nil, err 136 | } 137 | instances, err := InstancesNew(resp.Body) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return instances, nil 142 | } 143 | 144 | func containsGrade(arr []string, elem string) bool { 145 | for _, a := range arr { 146 | if grade.Symbol(a) == elem { 147 | return true 148 | } 149 | } 150 | return false 151 | } 152 | 153 | // Finds a random instance between the ones fetched according 154 | // to user's choosen options. 155 | func FindRandomInstance(fetchedInstances *map[string]Instance, gradesEnabled []string, blacklist []string, torEnabled bool, torOnlyEnabled bool, minVersion version.Version, customInstances []string) (*string, bool) { 156 | keys := *new([]string) 157 | LOOP: for key, instance := range *fetchedInstances { 158 | if instance.Error == nil && instance.Version != nil { 159 | if !containsGrade(gradesEnabled, (*instance.Html).Grade) { 160 | continue LOOP 161 | } 162 | 163 | for _, blacklisted := range blacklist { 164 | if len(strings.TrimSpace(blacklisted)) < 1 { 165 | continue 166 | } 167 | if r, err := regexp.Compile(blacklisted); err == nil && r.MatchString(key) { 168 | continue LOOP 169 | } 170 | } 171 | 172 | version, err := version.NewVersion(*instance.Version) 173 | if err == nil && minVersion.GreaterThan(version) { 174 | continue LOOP 175 | } 176 | 177 | if torEnabled && *instance.NetworkType == "tor" { 178 | keys = append(keys, key) 179 | } else if !torOnlyEnabled && *instance.NetworkType != "tor" { 180 | keys = append(keys, key) 181 | } 182 | } 183 | } 184 | for _, customInstance := range customInstances { 185 | keys = append(keys, customInstance) 186 | } 187 | 188 | if len(keys) < 1 { 189 | return nil, false 190 | } 191 | randInt := rand.Intn(len(keys)) 192 | randUrl := keys[randInt] 193 | 194 | isCustom := false 195 | for _, customInstance := range customInstances { 196 | if randUrl == customInstance { 197 | isCustom = true 198 | break 199 | } 200 | } 201 | 202 | return &randUrl, isCustom 203 | } 204 | 205 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package findlatestversion 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | "io/ioutil" 7 | ) 8 | 9 | // Sucky regex, but it works 10 | var r = regexp.MustCompile("tag/(.*?)\"") 11 | 12 | // Returns SearX's latest version. 13 | func Searx() string { 14 | resp, err := http.Get("https://github.com/searx/searx/releases") 15 | if err != nil { 16 | // In case the request to Github fails, 17 | // fallback to current version. Should 18 | // it error instead? 19 | return "0.18.0" 20 | } 21 | page, _ := ioutil.ReadAll(resp.Body) 22 | result := r.FindStringSubmatch(string(page)) 23 | return result[1] 24 | } 25 | 26 | -------------------------------------------------------------------------------- /opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | gimmeasearx 4 | Search SearX 5 | UTF-8 6 | SearX using gimmeasearx 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: demostanis worlds 2 | 3 | _pkgname="gimmeasearx" 4 | pkgname="$_pkgname-git" 5 | pkgver="2.0.0" 6 | pkgrel=1 7 | pkgdesc="Find a random searx instance" 8 | arch=("x86_64") 9 | url="https://github.com/demostanis/gimmeasearx" 10 | license=("GPL3") 11 | makedepends=("git" "go") 12 | optdepends=("tor: required to show .onion instances") 13 | conflicts=("gimmeasearx") 14 | provides=("gimmeasearx") 15 | source=("git+$url.git") 16 | sha512sums=("SKIP") 17 | 18 | build() { 19 | cd $_pkgname 20 | 21 | export CGO_CPPFLAGS="$CPPFLAGS" 22 | export CGO_CFLAGS="$CFLAGS" 23 | export CGO_CXXFLAGS="$CXXFLAGS" 24 | export CGO_LDFLAGS="$LDFLAGS" 25 | export GOFLAGS='-buildmode=pie -trimpath -mod=readonly -modcacherw' 26 | 27 | go build . 28 | } 29 | 30 | package() { 31 | install -Dm 755 /dev/stdin "$pkgdir"/usr/bin/$_pkgname < 2 | 3 | 4 | 5 | 6 | 114 | 115 | 116 |
117 |

Configuration panel

118 |
119 | 120 |
121 | 122 | 123 | 124 | 125 |

126 |
127 | 131 | 132 |

133 | 134 | {{$selected := .GradesSelected}} 135 | {{range $grade := .Grades}} 136 | {{.Symbol}} 137 | {{end}} 138 | 139 |

140 | 141 |
142 | ? 143 | Additional preferences string to send alongside the query. 144 | It can be found in a SearX instance under Preferences>Cookies, after ?preferences=. 145 |
146 | 147 | 148 |

149 | 150 |
151 | ? 152 | Additional parameters that will be added to the URL, 153 | of the form key=value&other_key=value. 154 | They can be found in a SearX instance under Preferences>Cookies. 155 |
156 | 157 | 158 |

159 | 160 | 161 | OR 162 | latest 163 | 164 |

165 |
166 |

170 | 171 | 172 |



173 |
174 |
175 | 176 |

gimmeasearx

177 |

Find a random searx instance

178 | {{if .Error}} 179 |

No instances available. Please try again in a few seconds.

180 | {{else}} 181 |
182 | 183 | 184 | {{range .Params}} 185 | 186 | {{end}} 187 | 188 |
189 | {{.InstanceUrl}} {{range .Instance.Comments}} 190 | {{.}} 191 | {{end}}{{if .Instance.Html}}| grade:
192 | {{.Instance.Html.Grade}} 193 | {{.GradeComment}}{{end}} 194 |
{{if .Instance.Version}}| version: {{.Instance.Version}}
{{end}} 195 | {{end}} 196 |
197 | 198 | 199 | 200 | --------------------------------------------------------------------------------