diff options
| -rw-r--r-- | roseh.moe.go | 115 | ||||
| -rw-r--r-- | static/htmx-ext-sse.js | 8 | ||||
| -rw-r--r-- | templates/wormhole.html.template | 32 |
3 files changed, 64 insertions, 91 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index 696b64e..3a53208 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/hmac" "crypto/rand" "crypto/sha256" @@ -13,6 +14,7 @@ import ( "html/template" "io" "log" + "mime/multipart" "net/http" "os" "strings" @@ -198,24 +200,59 @@ func makeHole() string { return strings.Join(words, "-") } +func newWormhole(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/wormhole/"+makeHole()+"/upload", http.StatusSeeOther) +} + func wormhole(w http.ResponseWriter, r *http.Request) { - if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: makeHole()}); err != nil { + if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: r.PathValue("hole")}); err != nil { log.Printf("Warning: wormhole: %s", err) } } type wormholeConn struct { done chan struct{} + r *multipart.Part w http.ResponseWriter } var wormholeConnsMu sync.Mutex -var wormholeConns = make(map[string]*wormholeConn) +var wormholeConns = make(map[string]wormholeConn) -var wormholeNotifyMu sync.Mutex -var wormholeNotify = make(map[string]chan struct{}) +func (c wormholeConn) wormholeCopy(ctx context.Context, hole string) error { + wormholeConnsMu.Lock() + prevConn, ok := wormholeConns[hole] + if !ok { + wormholeConns[hole] = c + wormholeConnsMu.Unlock() + defer func() { + wormholeConnsMu.Lock() + delete(wormholeConns, hole) + wormholeConnsMu.Unlock() + }() + select { + case <-ctx.Done(): + return ctx.Err() + case <-c.done: + return nil + } + } + wormholeConnsMu.Unlock() + defer close(prevConn.done) + if c.w == nil { + c.w = prevConn.w + } else { + c.r = prevConn.r + } + c.w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+c.r.FileName()) + if _, err := io.Copy(c.w, c.r); err != nil { + return fmt.Errorf("copy: %s", err) + } + return nil +} func wormholeSend(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() reader, err := r.MultipartReader() if err != nil { http.Error(w, fmt.Sprintf("Not a multipart/form-data request: %s", err), http.StatusBadRequest) @@ -230,19 +267,8 @@ func wormholeSend(w http.ResponseWriter, r *http.Request) { continue } hole := r.PathValue("hole") - wormholeConnsMu.Lock() - conn := wormholeConns[hole] - delete(wormholeConns, hole) - wormholeConnsMu.Unlock() - if conn == nil { - http.Error(w, "no such connection", http.StatusBadRequest) - return - } - defer close(conn.done) - conn.w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+part.FileName()) - if _, err := io.Copy(conn.w, part); err != nil { + if err := (wormholeConn{done: make(chan struct{}), r: part}).wormholeCopy(ctx, hole); err != nil { http.Error(w, fmt.Sprintf("Error during copy: %s", err), http.StatusServiceUnavailable) - return } fmt.Fprintf(w, "uploaded!") return @@ -253,56 +279,9 @@ func wormholeSend(w http.ResponseWriter, r *http.Request) { func wormholeRecv(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hole := r.PathValue("hole") - conn := &wormholeConn{ - done: make(chan struct{}), - w: w, - } - wormholeConnsMu.Lock() - wormholeConns[hole] = conn - wormholeConnsMu.Unlock() - defer func() { - wormholeConnsMu.Lock() - delete(wormholeConns, hole) - wormholeConnsMu.Unlock() - }() - wormholeNotifyMu.Lock() - notify := wormholeNotify[hole] - wormholeNotifyMu.Unlock() - if notify == nil { - http.Error(w, "no such connection", http.StatusBadRequest) - return - } - select { - case notify <- struct{}{}: - default: - http.Error(w, "connection not ready", http.StatusBadRequest) - return - } - select { - case <-ctx.Done(): - case <-conn.done: - } -} - -func wormholeReady(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - hole := r.PathValue("hole") - notify := make(chan struct{}) - wormholeNotifyMu.Lock() - wormholeNotify[hole] = notify - wormholeNotifyMu.Unlock() - defer func() { - wormholeNotifyMu.Lock() - delete(wormholeNotify, hole) - wormholeNotifyMu.Unlock() - }() - w.Header().Set("Content-Type", "text/event-stream") - select { - case <-ctx.Done(): - return - case <-notify: + if err := (wormholeConn{done: make(chan struct{}), w: w}).wormholeCopy(ctx, hole); err != nil { + http.Error(w, fmt.Sprintf("Error during copy: %s", err), http.StatusServiceUnavailable) } - fmt.Fprintf(w, "event: ready\ndata:\n\n") } func mac(msg []byte) []byte { @@ -551,10 +530,10 @@ func main() { } http.HandleFunc("GET /pong", pong) - http.HandleFunc("GET /wormhole", wormhole) - http.HandleFunc("POST /wormhole/{hole}", wormholeSend) + http.HandleFunc("GET /wormhole", newWormhole) + http.HandleFunc("GET /wormhole/{hole}/upload", wormhole) + http.HandleFunc("POST /wormhole/{hole}/upload", wormholeSend) http.HandleFunc("GET /wormhole/{hole}", wormholeRecv) - http.HandleFunc("GET /wormhole/{hole}/ready", wormholeReady) http.HandleFunc("POST /login", login) http.HandleFunc("GET /notepad", notepad) http.HandleFunc("POST /notepad", autosave) diff --git a/static/htmx-ext-sse.js b/static/htmx-ext-sse.js deleted file mode 100644 index 6d087ec..0000000 --- a/static/htmx-ext-sse.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Minified by jsDelivr using Terser v5.37.0. - * Original file: /npm/htmx-ext-sse@2.2.2/sse.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -!function(){var e;function t(e){return new EventSource(e,{withCredentials:!0})}function n(t){if(e.getAttributeValue(t,"sse-swap")){if(null==(u=e.getClosestMatch(t,i)))return null;for(var n=e.getInternalData(u).sseEventSource,r=e.getAttributeValue(t,"sse-swap").split(","),o=0;o<r.length;o++){const i=r[o].trim(),c=function(r){s(u)||(e.bodyContains(t)?e.triggerEvent(t,"htmx:sseBeforeMessage",r)&&(a(t,r.data),e.triggerEvent(t,"htmx:sseMessage",r)):n.removeEventListener(i,c))};e.getInternalData(t).sseEventListener=c,n.addEventListener(i,c)}}if(e.getAttributeValue(t,"hx-trigger")){var u;if(null==(u=e.getClosestMatch(t,i)))return null;n=e.getInternalData(u).sseEventSource;e.getTriggerSpecs(t).forEach((function(r){if("sse:"===r.trigger.slice(0,4)){var a=function(i){s(u)||(e.bodyContains(t)||n.removeEventListener(r.trigger.slice(4),a),htmx.trigger(t,r.trigger,i),htmx.trigger(t,"htmx:sseMessage",i))};e.getInternalData(t).sseEventListener=a,n.addEventListener(r.trigger.slice(4),a)}}))}}function r(t,a){if(null==t)return null;if(e.getAttributeValue(t,"sse-connect")){var i=e.getAttributeValue(t,"sse-connect");if(null==i)return;!function(t,a,i){var o=htmx.createEventSource(a);o.onerror=function(n){if(e.triggerErrorEvent(t,"htmx:sseError",{error:n,source:o}),!s(t)&&o.readyState===EventSource.CLOSED){i=i||0;var a=500*(i=Math.max(Math.min(2*i,128),1));window.setTimeout((function(){r(t,i)}),a)}},o.onopen=function(r){if(e.triggerEvent(t,"htmx:sseOpen",{source:o}),i&&i>0){const e=t.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let t=0;t<e.length;t++)n(e[t]);i=0}},e.getInternalData(t).sseEventSource=o;var u=e.getAttributeValue(t,"sse-close");u&&o.addEventListener(u,(function(){e.triggerEvent(t,"htmx:sseClose",{source:o,type:"message"}),o.close()}))}(t,i,a)}n(t)}function s(t){if(!e.bodyContains(t)){var n=e.getInternalData(t).sseEventSource;if(null!=n)return e.triggerEvent(t,"htmx:sseClose",{source:n,type:"nodeMissing"}),n.close(),!0}return!1}function a(t,n){e.withExtensions(t,(function(e){n=e.transformResponse(n,null,t)}));var r=e.getSwapSpecification(t),s=e.getTarget(t);e.swap(s,n,r)}function i(t){return null!=e.getInternalData(t).sseEventSource}htmx.defineExtension("sse",{init:function(n){e=n,null==htmx.createEventSource&&(htmx.createEventSource=t)},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(t,n){var s=n.target||n.detail.elt;switch(t){case"htmx:beforeCleanupElement":var a=e.getInternalData(s),i=a.sseEventSource;return void(i&&(e.triggerEvent(s,"htmx:sseClose",{source:i,type:"nodeReplaced"}),a.sseEventSource.close()));case"htmx:afterProcessNode":r(s)}}})}(); -//# sourceMappingURL=/sm/97a20e642efcf59a346095b79d9e73120e258a50836a64ba297a1990d62f8cd5.map
\ No newline at end of file diff --git a/templates/wormhole.html.template b/templates/wormhole.html.template index 4d67a98..5703949 100644 --- a/templates/wormhole.html.template +++ b/templates/wormhole.html.template @@ -1,19 +1,21 @@ {{define "head"}} - <script src="/static/htmx.min.js"></script> - <script src="/static/htmx-ext-sse.js"></script> <title>Wormhole</title> {{end}} -<div - hx-ext="sse" - sse-connect="/wormhole/{{.Hole}}/ready"> - <form - hx-post="/wormhole/{{.Hole}}" - hx-trigger="sse:ready" - enctype="multipart/form-data"> - <label for="file-input">1. Choose file</label> - <input id="file-input" type="file" name="file"> - </form> -</div> - -<p>2. Visit this URL <a href="{{.Self}}/wormhole/{{.Hole}}">{{.Self}}/wormhole/{{.Hole}}</a></p> +<form + method="post" + enctype="multipart/form-data"> + <ol> + <li> + <label for="file-input">Choose file</label> + <input id="file-input" type="file" name="file"> + </li> + <li> + <p>Visit this URL <a href="/wormhole/{{.Hole}}">{{.Self}}/wormhole/{{.Hole}}</a></p> + </li> + <li> + <label for="submit">Click here:</label> + <input id="submit" type="submit" value="Upload"> + </li> + </ol> +</form> |
