Move Http utils to jrw.util.
[jrw.git] / src / jrw / FormData.java
1 package jrw;
2
3 import jrw.util.*;
4 import java.util.*;
5 import java.util.function.*;
6 import java.io.*;
7 import java.nio.*;
8 import java.nio.channels.*;
9
10 public class FormData extends HashMap<String, String> {
11     public static final int MAX_LENGTH = 1 << 20;
12
13     private static int htoi(byte hex) {
14         if((hex >= '0') && (hex <= '9'))
15             return(hex - '0');
16         if((hex >= 'A') && (hex <= 'F'))
17             return(hex - 'A' + 10);
18         if((hex >= 'a') && (hex <= 'f'))
19             return(hex - 'a' + 10);
20         return(0);
21     }
22
23     private static ByteBuffer unquoteb(ByteBuffer part) {
24         ByteBuffer ret = ByteBuffer.allocate(part.remaining());
25         while(part.remaining() > 0) {
26             int b = part.get() & 0xff;
27             if((b == '%') && (part.remaining() >= 2)) {
28                 int n1 = htoi(part.get()), n2 = htoi(part.get());
29                 ret.put((byte)((n1 << 4) | n2));
30             } else {
31                 ret.put((byte)b);
32             }
33         }
34         ret.flip();
35         return(ret);
36     }
37
38     private static String unquote(ByteBuffer part) {
39         ByteBuffer dec = unquoteb(part);
40         try {
41             return(Http.UTF8.newDecoder().decode(dec.duplicate()).toString());
42         } catch(java.nio.charset.CharacterCodingException e) {
43             return(Http.LATIN1.decode(dec).toString());
44         }
45     }
46
47     private static String unquote(String part) {
48         if(part.indexOf('%') < 0)
49             return(part);
50         return(unquote(Http.UTF8.encode(CharBuffer.wrap(part))));
51     }
52
53     public static void parse(Map<? super String, ? super String> buf, ByteBuffer data) {
54         int p = data.position(), p2, p3;
55         while(p < data.limit()) {
56             for(p2 = p; (p2 < data.limit()) && (data.get(p2) != '&'); p2++);
57             for(p3 = p; (p3 < p2) && (data.get(p3) != '='); p3++);
58             if(p3 < p2) {
59                 buf.put(unquote((ByteBuffer)data.duplicate().position(p).limit(p3)),
60                         unquote((ByteBuffer)data.duplicate().position(p3 + 1).limit(p2)));
61             }
62             p = p2 + 1;
63         }
64     }
65
66     public static void parse(Map<? super String, ? super String> buf, String data) {
67         int p = 0;
68         while(true) {
69             int p2 = data.indexOf('&', p);
70             String part = (p2 < 0) ? data.substring(p) : data.substring(p, p2);
71             int p3 = part.indexOf('=');
72             if(p3 >= 0)
73                 buf.put(unquote(part.substring(0, p3)), unquote(part.substring(p3 + 1)));
74             if(p2 < 0)
75                 break;
76             p = p2 + 1;
77         }
78     }
79
80     public static FormData read(Request req) {
81         FormData ret = new FormData();
82         String query = (String)req.env.get("QUERY_STRING");
83         if(query != null)
84             parse(ret, query);
85         if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
86             int max = MAX_LENGTH;
87             String clen = req.ihead("Content-Length", null);
88             if(clen != null) {
89                 try {
90                     max = Math.min(max, Integer.parseInt(clen));
91                 } catch(NumberFormatException e) {
92                 }
93             }
94             ReadableByteChannel in = (ReadableByteChannel)req.env.get("jagi.input");
95             if(in instanceof SelectableChannel) {
96                 try {
97                     ((SelectableChannel)in).configureBlocking(true);
98                 } catch(IOException e) {
99                 }
100             }
101             ByteBuffer buf = ByteBuffer.allocate(65536);
102             while(buf.position() < max) {
103                 if(buf.remaining() == 0) {
104                     ByteBuffer n = ByteBuffer.allocate(Math.min(buf.capacity() * 2, max));
105                     buf.flip();
106                     n.put(buf);
107                     buf = n;
108                 }
109                 try {
110                     int rv = in.read(buf);
111                     if(rv <= 0)
112                         break;
113                 } catch(IOException e) {
114                     break;
115                 }
116             }
117             buf.flip();
118             parse(ret, buf);
119         }
120         return(ret);
121     }
122
123     public static FormData get(Request req) {
124         FormData ret = (FormData)req.env.get(FormData.class);
125         if(ret == null)
126             req.env.put(FormData.class, ret = read(req));
127         return(ret);
128     }
129
130     static class Collector extends ByteArrayOutputStream {
131         final FormData form = new FormData();
132         final Request req;
133
134         Collector(Request req) {
135             this.req = req;
136             req.env.put(FormData.class, this.form);
137             String query = (String)req.env.get("QUERY_STRING");
138             if(query != null)
139                 parse(form, query);
140         }
141
142         public void write(int b) {
143             if(count < MAX_LENGTH)
144                 super.write(b);
145         }
146
147         public void write(byte[] buf, int off, int len) {
148             len = Math.min(len, MAX_LENGTH - count);
149             if(len > 0)
150                 super.write(buf, off, len);
151         }
152
153         public void close() {
154             parse(form, ByteBuffer.wrap(toByteArray()));
155         }
156     }
157
158     public static Map<Object, Object> feed(Request req, Handler next) {
159         Map<Object, Object> resp = new HashMap<>();
160         if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
161             resp.put("jagi.status", "feed-input");
162             resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Dispatch.handle(next, req));
163             resp.put("jagi.input-sink", new Collector(req));
164         } else {
165             read(req);
166             resp.put("jagi.status", "chain");
167             resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Dispatch.handle(next, req));
168         }
169         return(resp);
170     }
171 }