public
Description: Phusion Passenger (mod_rails)
Homepage: http://www.modrails.com/
Clone URL: git://github.com/FooBarWidget/passenger.git
Search Repo:
Click here to lend your support to: passenger and make a donation at www.pledgie.com !
- Refactor the Hook class, and make it more 'C++-ey'. I.e. make less use 
of static global variables.
- Make sure the Apache module is only initialized once. Apparently Apache 
initializes modules twice.
- Optimize the request handler's protocol a bit so that fewer system calls 
are necessary.
FooBarWidget (author)
Fri Feb 01 05:04:31 -0800 2008
commit  86a183863f623e835fb54c615dda3ef9edd1386e
tree    d9deef9519fe8347e9db509419b2f64bec988359
parent  0df1814d5800bb379c4e405989bb70b7b104d3d6
...
23
24
25
26
 
27
28
29
30
31
32
 
33
34
35
...
57
58
59
60
 
61
62
63
...
65
66
67
68
 
69
70
71
...
23
24
25
 
26
27
28
29
30
31
 
32
33
34
35
...
57
58
59
 
60
61
62
63
...
65
66
67
 
68
69
70
71
0
@@ -23,13 +23,13 @@
0
     this->pid = pid;
0
     this->reader = reader;
0
     this->writer = writer;
0
- P_DEBUG("Application " << this << ": created.");
0
+ P_TRACE("Application " << this << ": created.");
0
   }
0
   
0
   ~Application() {
0
     closeReader();
0
     closeWriter();
0
- P_DEBUG("Application " << this << ": destroyed.");
0
+ P_TRACE("Application " << this << ": destroyed.");
0
   }
0
   
0
   void detachCommunicationChannels() {
0
@@ -57,7 +57,7 @@
0
     if (reader != -1) {
0
       close(reader);
0
       reader = -1;
0
- P_DEBUG("Application " << this << ": reader closed.");
0
+ P_TRACE("Application " << this << ": reader closed.");
0
     }
0
   }
0
   
0
@@ -65,7 +65,7 @@
0
     if (writer != -1) {
0
       close(writer);
0
       writer = -1;
0
- P_DEBUG("Application " << this << ": writer closed.");
0
+ P_TRACE("Application " << this << ": writer closed.");
0
     }
0
   }
0
 };
...
26
27
28
29
30
 
 
 
 
 
31
32
33
...
26
27
28
 
 
29
30
31
32
33
34
35
36
0
@@ -26,8 +26,11 @@
0
   }
0
   
0
 public:
0
- ApplicationPool(const string &spawnManagerCommand, const string &logFile = "")
0
- : spawnManager(spawnManagerCommand, logFile) {}
0
+ ApplicationPool(const string &spawnManagerCommand,
0
+ const string &logFile = "",
0
+ const string &environment = "production",
0
+ const string &rubyCommand = "ruby")
0
+ : spawnManager(spawnManagerCommand, logFile, environment, rubyCommand) {}
0
   
0
   ApplicationPtr get(const string &appRoot) {
0
     return get(appRoot, "");
...
136
137
138
139
 
140
141
142
143
144
145
 
146
147
148
149
150
...
156
157
158
159
 
160
161
162
 
163
164
165
166
167
168
 
169
170
171
...
198
199
200
201
 
202
203
204
...
211
212
213
214
 
215
216
217
...
136
137
138
 
139
140
141
142
143
144
 
145
146
147
148
149
150
...
156
157
158
 
159
160
161
 
162
163
164
165
166
167
 
168
169
170
171
...
198
199
200
 
201
202
203
204
...
211
212
213
 
214
215
216
217
0
@@ -136,13 +136,13 @@
0
     
0
     result = read_chunk_size(chunk_size, current_timeout);
0
     if (result == APR_EOF || (result == APR_SUCCESS && chunk_size == 0)) {
0
- P_DEBUG("DispatcherBucket " << this << ": EOF");
0
+ P_TRACE("DispatcherBucket " << this << ": EOF");
0
       b = apr_bucket_immortal_make(b, "", 0);
0
       *str = (const char *) b->data;
0
       app->closeReader();
0
       return APR_SUCCESS;
0
     } else if (result != APR_SUCCESS) {
0
- P_DEBUG("DispatcherBucket " << this << ": APR error " << result);
0
+ P_TRACE("DispatcherBucket " << this << ": APR error " << result);
0
       return result;
0
     }
0
 
0
0
0
@@ -156,16 +156,16 @@
0
       *str = chunk;
0
       *len = chunk_size;
0
       APR_BUCKET_INSERT_AFTER(b, dup_bucket(b->list));
0
- P_DEBUG("DispatcherBucket " << this << ": read (" << string(*str, *len) << ")");
0
+ P_TRACE("DispatcherBucket " << this << ": read (" << string(*str, *len) << ")");
0
       return APR_SUCCESS;
0
     } else if (result == APR_EOF) {
0
- P_DEBUG("DispatcherBucket " << this << ": EOF");
0
+ P_TRACE("DispatcherBucket " << this << ": EOF");
0
       b = apr_bucket_immortal_make(b, "", 0);
0
       *str = (const char *) b->data;
0
       app->closeReader();
0
       return APR_SUCCESS;
0
     } else {
0
- P_DEBUG("DispatcherBucket " << this << ": APR error " << result);
0
+ P_TRACE("DispatcherBucket " << this << ": APR error " << result);
0
       return result;
0
     }
0
   }
0
@@ -198,7 +198,7 @@
0
   b->data = data;
0
   apr_pool_cleanup_register(pool, data, dispatcher_bucket_pool_cleaner, apr_pool_cleanup_null);
0
   
0
- P_DEBUG("DispatcherBucket " << data << " created.");
0
+ P_TRACE("DispatcherBucket " << data << " created.");
0
   return b;
0
 }
0
 
0
@@ -211,7 +211,7 @@
0
 dispatcher_bucket_destroy(void *d) {
0
   DispatcherBucket *data = (DispatcherBucket *) d;
0
   data->app = ApplicationPtr();
0
- P_DEBUG("DispatcherBucket " << d << " destroyed.");
0
+ P_TRACE("DispatcherBucket " << d << " destroyed.");
0
 }
0
 
0
 static apr_status_t
...
 
 
 
 
 
1
2
3
...
8
9
10
 
11
12
13
...
27
28
29
30
31
 
 
32
33
34
35
...
62
63
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
67
68
...
87
88
89
90
 
91
92
93
...
98
99
100
101
102
 
 
103
104
105
...
111
112
113
114
 
115
116
117
118
119
120
121
122
123
...
126
127
128
 
129
130
131
132
133
134
 
 
 
135
136
 
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
 
 
 
 
 
161
162
163
164
 
165
166
167
168
 
 
 
 
 
 
 
 
169
170
171
...
191
192
193
 
 
194
195
196
...
202
203
204
205
 
206
207
208
209
210
211
212
213
214
215
...
258
259
260
261
262
 
 
 
263
 
 
 
 
 
 
 
 
264
265
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
268
 
 
 
 
 
 
 
269
270
271
 
 
 
 
 
272
273
274
275
276
 
 
 
 
 
277
278
279
280
281
 
 
282
283
284
...
1
2
3
4
5
6
7
8
...
13
14
15
16
17
18
19
...
33
34
35
 
 
36
37
38
39
40
41
...
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
...
158
159
160
 
161
162
163
164
...
169
170
171
 
 
172
173
174
175
176
...
182
183
184
 
185
186
187
188
189
190
191
192
193
194
...
197
198
199
200
201
202
203
 
 
 
204
205
206
207
 
208
209
210
211
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
 
 
 
214
215
216
217
218
219
220
 
 
221
222
223
 
 
224
225
226
227
228
229
230
231
232
233
234
...
254
255
256
257
258
259
260
261
...
267
268
269
 
270
271
272
273
274
275
276
277
278
279
280
...
323
324
325
 
326
327
328
329
330
331
332
333
334
335
336
337
338
339
 
 
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
 
377
378
379
380
381
382
383
384
385
 
386
387
388
389
390
391
392
393
394
 
395
396
397
398
399
0
@@ -1,3 +1,8 @@
0
+/*
0
+ * A few functions are based on the code from mod_scgi 1.9. mod_scgi is
0
+ * Copyright (c) 2004 Corporation for National Research Initiatives; All Rights Reserved
0
+ */
0
+
0
 #include <ap_config.h>
0
 #include <httpd.h>
0
 #include <http_config.h>
0
@@ -8,6 +13,7 @@
0
 #include <util_script.h>
0
 #include <apr_pools.h>
0
 #include <apr_strings.h>
0
+#include <apr_lib.h>
0
 
0
 #include <exception>
0
 #include <unistd.h>
0
@@ -27,8 +33,8 @@
0
 
0
 class Hooks {
0
 private:
0
- static ApplicationPoolPtr applicationPool;
0
-
0
+ ApplicationPoolPtr applicationPool;
0
+
0
   RailsConfig *getConfig(request_rec *r) {
0
     return (RailsConfig *) ap_get_module_config(r->per_dir_config, &rails_module);
0
   }
0
0
@@ -62,7 +68,72 @@
0
   bool verifyRailsDir(apr_pool_t *pool, const char *dir) {
0
     return fileExists(pool, apr_pstrcat(pool, dir, "/../config/environment.rb", NULL));
0
   }
0
+
0
+ char *http2env(apr_pool_t *p, const char *name) {
0
+ char *env_name = apr_pstrcat(p, "HTTP_", name, NULL);
0
+ char *cp;
0
+
0
+ for (cp = env_name + 5; *cp != 0; cp++) {
0
+ if (*cp == '-') {
0
+ *cp = '_';
0
+ } else {
0
+ *cp = apr_toupper(*cp);
0
+ }
0
+ }
0
+
0
+ return env_name;
0
+ }
0
+
0
+ char *lookupName(apr_table_t *t, const char *name) {
0
+ const apr_array_header_t *hdrs_arr = apr_table_elts(t);
0
+ apr_table_entry_t *hdrs = (apr_table_entry_t *) hdrs_arr->elts;
0
+ int i;
0
+
0
+ for (i = 0; i < hdrs_arr->nelts; ++i) {
0
+ if (hdrs[i].key == NULL) {
0
+ continue;
0
+ }
0
+ if (strcasecmp(hdrs[i].key, name) == 0) {
0
+ return hdrs[i].val;
0
+ }
0
+ }
0
+ return NULL;
0
+ }
0
+
0
+ char *lookupHeader(request_rec *r, const char *name) {
0
+ return lookupName(r->headers_in, name);
0
+ }
0
+
0
+ char *lookupEnv(request_rec *r, const char *name) {
0
+ return lookupName(r->subprocess_env, name);
0
+ }
0
+
0
+ // This code is a duplicate of what's in util_script.c. We can't use
0
+ // r->unparsed_uri because it gets changed if there was a redirect.
0
+ char *originalURI(request_rec *r) {
0
+ char *first, *last;
0
 
0
+ if (r->the_request == NULL) {
0
+ return (char *) apr_pcalloc(r->pool, 1);
0
+ }
0
+
0
+ first = r->the_request; // use the request-line
0
+
0
+ while (*first && !apr_isspace(*first)) {
0
+ ++first; // skip over the method
0
+ }
0
+ while (apr_isspace(*first)) {
0
+ ++first; // and the space(s)
0
+ }
0
+
0
+ last = first;
0
+ while (*last && !apr_isspace(*last)) {
0
+ ++last; // end at next whitespace
0
+ }
0
+
0
+ return apr_pstrmemdup(r->pool, first, last - first);
0
+ }
0
+
0
   void addHeader(apr_table_t *table, const char *name, const char *value) {
0
     if (name != NULL && value != NULL) {
0
       apr_table_addn(table, name, value);
0
@@ -87,7 +158,7 @@
0
     addHeader(headers, "REMOTE_PORT", apr_psprintf(r->pool, "%d", r->connection->remote_addr->port));
0
     addHeader(headers, "REMOTE_USER", r->user);
0
     addHeader(headers, "REQUEST_METHOD", r->method);
0
- //addHeader(headers, "REQUEST_URI", original_uri(r));
0
+ addHeader(headers, "REQUEST_URI", originalURI(r));
0
     addHeader(headers, "QUERY_STRING", r->args ? r->args : "");
0
     addHeader(headers, "SCRIPT_NAME", r->uri);
0
     if (r->path_info) {
0
@@ -98,8 +169,8 @@
0
     } else {
0
       addHeader(headers, "SCRIPT_NAME", r->uri);
0
     }
0
- //addHeader(headers, "HTTPS", lookup_env(r, "HTTPS"));
0
- //addHeader(headers, "CONTENT_TYPE", lookup_header(r, "Content-type"));
0
+ addHeader(headers, "HTTPS", lookupEnv(r, "HTTPS"));
0
+ addHeader(headers, "CONTENT_TYPE", lookupHeader(r, "Content-type"));
0
     addHeader(headers, "DOCUMENT_ROOT", ap_document_root(r));
0
   
0
     // Set HTTP headers.
0
@@ -111,7 +182,7 @@
0
     hdrs = (apr_table_entry_t *) hdrs_arr->elts;
0
     for (i = 0; i < hdrs_arr->nelts; ++i) {
0
       if (hdrs[i].key) {
0
- //addHeader(headers, http2env(r->pool, hdrs[i].key), hdrs[i].val);
0
+ addHeader(headers, http2env(r->pool, hdrs[i].key), hdrs[i].val);
0
       }
0
     }
0
   
0
0
0
0
0
0
0
@@ -126,46 +197,38 @@
0
     }
0
     
0
     MessageChannel channel(fd);
0
+ list<string> entries;
0
     hdrs_arr = apr_table_elts(headers);
0
         hdrs = (apr_table_entry_t*) hdrs_arr->elts;
0
     for (i = 0; i < hdrs_arr->nelts; ++i) {
0
- // TODO: Internally, this creates a new list every time.
0
- // It can be optimized by using an array or a vector.
0
- channel.write(hdrs[i].key, hdrs[i].val, NULL);
0
+ // TODO: optimize this. too much copying.
0
+ entries.push_back(hdrs[i].key);
0
+ entries.push_back(hdrs[i].val);
0
     }
0
- channel.write("", NULL);
0
+ channel.write(entries);
0
   
0
     return APR_SUCCESS;
0
   }
0
 
0
- void
0
- debug(const char *format, ...) {
0
- va_list ap;
0
- char message[1024];
0
-
0
- va_start(ap, format);
0
- int size = apr_vsnprintf(message, sizeof(message), format, ap);
0
- FILE *f = fopen("/dev/pts/3", "w");
0
- if (f != NULL) {
0
- fwrite(message, 1, size, f);
0
- fclose(f);
0
- }
0
- va_end(ap);
0
- }
0
-
0
 public:
0
- int
0
- init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *base_server) {
0
- initDebugging("/tmp/passenger.txt");
0
- ap_add_version_component(p, "Phusion_Passenger/" PASSENGER_VERSION);
0
+ Hooks(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) {
0
+ initDebugging();
0
+ P_DEBUG("Initializing mod_passenger.");
0
+ ap_add_version_component(pconf, "Phusion_Passenger/" PASSENGER_VERSION);
0
+
0
     const char *spawnManagerCommand = "/home/hongli/Projects/mod_rails/lib/mod_rails/spawn_manager.rb";
0
     const char *logFile = "/home/hongli/Projects/mod_rails/spawner_log.txt";
0
- applicationPool = ApplicationPoolPtr(new ApplicationPool(spawnManagerCommand, logFile));
0
- return OK;
0
+ applicationPool = ApplicationPoolPtr(new ApplicationPool(spawnManagerCommand, logFile, "production"));
0
   }
0
   
0
- int
0
- handleRequest(request_rec *r) {
0
+ ~Hooks() {
0
+ P_DEBUG("Shutting down mod_passenger.");
0
+ }
0
+
0
+ void initChild(apr_pool_t *pchild, server_rec *s) {
0
+ }
0
+
0
+ int handleRequest(request_rec *r) {
0
     // The main request handler hook function.
0
     RailsConfig *config = getConfig(r);
0
     const char *railsDir;
0
@@ -191,6 +254,8 @@
0
       return OK;
0
     }
0
     
0
+
0
+
0
     /* int httpStatus = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR);
0
         if (httpStatus != OK) {
0
       return httpStatus;
0
@@ -202,7 +267,7 @@
0
       
0
       P_DEBUG("Processing HTTP request: " << r->uri);
0
       ApplicationPtr app(applicationPool->get(string(railsDir) + "/.."));
0
- P_DEBUG("Connected to application: reader FD = " << app->getReader() << ", writer FD = " << app->getWriter());
0
+ P_TRACE("Connected to application: reader FD = " << app->getReader() << ", writer FD = " << app->getWriter());
0
       sendHeaders(r, app->getWriter());
0
 
0
       bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc);
0
0
0
0
0
0
0
0
@@ -258,27 +323,77 @@
0
 };
0
 
0
 
0
-ApplicationPoolPtr Hooks::applicationPool;
0
 
0
+/******************************************************************
0
+ * Below follows lightweight C wrappers around the C++ Hook class.
0
+ ******************************************************************/
0
 
0
+static Hooks *hooks = NULL;
0
+
0
+static apr_status_t
0
+destroy_hooks(void *arg) {
0
+ delete hooks;
0
+ return APR_SUCCESS;
0
+}
0
+
0
 static int
0
-init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *base_server) {
0
- return Hooks().init(p, plog, ptemp, base_server);
0
+init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) {
0
+ /*
0
+ * 1. Apache on Unix calls the post_config hook twice, once before detach() and once
0
+ * after. On Windows it never calls detach().
0
+ * 2. When Apache is compiled to use DSO modules, the modules are unloaded between the
0
+ * two post_config hook calls.
0
+ * 3. On Unix, if the -X commandline option is given, detach() will not be called.
0
+ *
0
+ * Because of these 3 issues (and especially #2), we only want to intialize the second
0
+ * time the post_config hook is called.
0
+ */
0
+ void *firstInitCall = NULL;
0
+ apr_pool_t *processPool = s->process->pool;
0
+
0
+ apr_pool_userdata_get(&firstInitCall, "mod_passenger", processPool);
0
+ if (firstInitCall == NULL) {
0
+ apr_pool_userdata_set((const void *) 1, "mod_passenger",
0
+ apr_pool_cleanup_null, processPool);
0
+ return OK;
0
+ } else {
0
+ hooks = new Hooks(pconf, plog, ptemp, s);
0
+ apr_pool_cleanup_register(pconf, NULL,
0
+ destroy_hooks,
0
+ apr_pool_cleanup_null);
0
+ return OK;
0
+ }
0
 }
0
 
0
+static void
0
+init_child(apr_pool_t *pchild, server_rec *s) {
0
+ if (hooks != NULL) {
0
+ return hooks->initChild(pchild, s);
0
+ }
0
+}
0
+
0
 static int
0
 handle_request(request_rec *r) {
0
- return Hooks().handleRequest(r);
0
+ if (hooks != NULL) {
0
+ return hooks->handleRequest(r);
0
+ } else {
0
+ return DECLINED;
0
+ }
0
 }
0
 
0
 static int
0
 map_to_storage(request_rec *r) {
0
- return Hooks().mapToStorage(r);
0
+ if (hooks != NULL) {
0
+ return hooks->mapToStorage(r);
0
+ } else {
0
+ return DECLINED;
0
+ }
0
 }
0
 
0
 void
0
 passenger_register_hooks(apr_pool_t *p) {
0
- ap_hook_post_config(init, NULL, NULL, APR_HOOK_MIDDLE);
0
+ ap_hook_post_config(init_module, NULL, NULL, APR_HOOK_MIDDLE);
0
+ ap_hook_child_init(init_child, NULL, NULL, APR_HOOK_MIDDLE);
0
   ap_hook_map_to_storage(map_to_storage, NULL, NULL, APR_HOOK_FIRST);
0
   ap_hook_handler(handle_request, NULL, NULL, APR_HOOK_FIRST);
0
 }
...
32
33
34
35
 
 
 
 
36
37
38
 
39
40
41
42
...
63
64
65
 
 
 
66
67
68
 
69
70
71
...
32
33
34
 
35
36
37
38
39
40
 
41
42
43
44
45
...
66
67
68
69
70
71
72
73
 
74
75
76
77
0
@@ -32,10 +32,13 @@
0
   pid_t pid;
0
 
0
 public:
0
- SpawnManager(const string &spawnManagerCommand, const string &logFile = "", const string &rubyCommand = "ruby") {
0
+ SpawnManager(const string &spawnManagerCommand,
0
+ const string &logFile = "",
0
+ const string &environment = "production",
0
+ const string &rubyCommand = "ruby") {
0
     int fds[2];
0
     char fd_string[20];
0
- FILE *logFileHandle;
0
+ FILE *logFileHandle = NULL;
0
     
0
     if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) {
0
       throw SystemException("Cannot create a Unix socket", errno);
0
0
@@ -63,9 +66,12 @@
0
           dup2(fileno(logFileHandle), STDERR_FILENO);
0
           fclose(logFileHandle);
0
         }
0
+ if (!environment.empty()) {
0
+ setenv("RAILS_ENV", environment.c_str(), true);
0
+ }
0
         close(fds[0]);
0
         execlp(rubyCommand.c_str(), rubyCommand.c_str(), spawnManagerCommand.c_str(), fd_string, NULL);
0
- fprintf(stderr, "Unable to run ruby: %s\n", strerror(errno));
0
+ fprintf(stderr, "Unable to run %s: %s\n", rubyCommand.c_str(), strerror(errno));
0
         _exit(1);
0
       } else if (pid == -1) {
0
         fprintf(stderr, "Unable to fork a process: %s\n", strerror(errno));
...
14
15
16
 
17
18
 
19
20
21
...
14
15
16
17
18
19
20
21
22
23
0
@@ -14,8 +14,10 @@
0
           expr << std::endl; \
0
       } \
0
     } while (false)
0
+ #define P_TRACE P_DEBUG
0
 #else
0
   #define P_DEBUG(expr) do { /* nothing */ } while (false)
0
+ #define P_TRACE P_DEBUG
0
 #endif
0
 
0
   // Internal; do not use directly.
...
53
54
55
56
 
 
 
 
57
58
59
60
61
62
63
 
 
 
 
 
 
 
 
 
64
65
66
67
68
69
70
71
...
53
54
55
 
56
57
58
59
60
 
 
 
 
 
 
61
62
63
64
65
66
67
68
69
70
 
 
 
 
71
72
73
0
@@ -53,19 +53,21 @@
0
   end
0
   
0
   def process_next_request
0
- content = "hello <b>world</b>! #{rand}<br>\n"
0
+ content = "hello <b>world</b>!<br>\n"
0
+ content << "env = #{RAILS_ENV}<br>\n"
0
+ content << "pid = #{$$}<br>\n"
0
+ content << "rand = #{rand}<br>\n"
0
 
0
- done = false
0
- while !done
0
- header, value = @reader_channel.read
0
- if header.nil?
0
- @done = true
0
- return
0
+ headers = @reader_channel.read
0
+ if headers.nil?
0
+ @done = true
0
+ return
0
+ else
0
+ i = 0
0
+ while i < headers.size
0
+ content << "<tt>#{headers[i]} = #{headers[i + 1]}</tt><br>\n"
0
+ i += 2
0
       end
0
- if !header.empty?
0
- content << "<tt>#{header} = #{value}</tt><br>\n"
0
- end
0
- done = header.empty?
0
     end
0
     
0
     write_chunk("Status: 200 OK\r\n")

Comments

    No one has commented yet.